├── .github ├── dependabot.yml └── workflows │ ├── no-response.yml │ ├── publish.yaml │ └── test-package.yml ├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── lib └── fake_async.dart ├── pubspec.yaml └── test └── fake_async_test.dart /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration file. 2 | # See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates 3 | version: 2 4 | 5 | updates: 6 | - package-ecosystem: github-actions 7 | directory: / 8 | schedule: 9 | interval: monthly 10 | labels: 11 | - autosubmit 12 | groups: 13 | github-actions: 14 | patterns: 15 | - "*" 16 | -------------------------------------------------------------------------------- /.github/workflows/no-response.yml: -------------------------------------------------------------------------------- 1 | # A workflow to close issues where the author hasn't responded to a request for 2 | # more information; see https://github.com/actions/stale. 3 | 4 | name: No Response 5 | 6 | # Run as a daily cron. 7 | on: 8 | schedule: 9 | # Every day at 8am 10 | - cron: '0 8 * * *' 11 | 12 | # All permissions not specified are set to 'none'. 13 | permissions: 14 | issues: write 15 | pull-requests: write 16 | 17 | jobs: 18 | no-response: 19 | runs-on: ubuntu-latest 20 | if: ${{ github.repository_owner == 'dart-lang' }} 21 | steps: 22 | - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e 23 | with: 24 | # Don't automatically mark inactive issues+PRs as stale. 25 | days-before-stale: -1 26 | # Close needs-info issues and PRs after 14 days of inactivity. 27 | days-before-close: 14 28 | stale-issue-label: "needs-info" 29 | close-issue-message: > 30 | Without additional information we're not able to resolve this issue. 31 | Feel free to add more info or respond to any questions above and we 32 | can reopen the case. Thanks for your contribution! 33 | stale-pr-label: "needs-info" 34 | close-pr-message: > 35 | Without additional information we're not able to resolve this PR. 36 | Feel free to add more info or respond to any questions above. 37 | Thanks for your contribution! 38 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | # A CI configuration to auto-publish pub packages. 2 | 3 | name: Publish 4 | 5 | on: 6 | pull_request: 7 | branches: [ master ] 8 | push: 9 | tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] 10 | 11 | jobs: 12 | publish: 13 | if: ${{ github.repository_owner == 'dart-lang' }} 14 | uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main 15 | permissions: 16 | id-token: write # Required for authentication using OIDC 17 | pull-requests: write # Required for writing the pull request note 18 | -------------------------------------------------------------------------------- /.github/workflows/test-package.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | # Run on PRs and pushes to the default branch. 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | schedule: 10 | - cron: "0 0 * * 0" 11 | 12 | env: 13 | PUB_ENVIRONMENT: bot.github 14 | 15 | jobs: 16 | # Check code formatting and static analysis on a single OS (linux) 17 | # against Dart dev. 18 | analyze: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | sdk: [dev] 24 | steps: 25 | - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 26 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 27 | with: 28 | sdk: ${{ matrix.sdk }} 29 | - id: install 30 | name: Install dependencies 31 | run: dart pub get 32 | - name: Check formatting 33 | run: dart format --output=none --set-exit-if-changed . 34 | if: always() && steps.install.outcome == 'success' 35 | - name: Analyze code 36 | run: dart analyze --fatal-infos 37 | if: always() && steps.install.outcome == 'success' 38 | 39 | # Run tests on a matrix consisting of two dimensions: 40 | # 1. OS: ubuntu-latest, (macos-latest, windows-latest) 41 | # 2. release channel: dev 42 | test: 43 | needs: analyze 44 | runs-on: ${{ matrix.os }} 45 | strategy: 46 | fail-fast: false 47 | matrix: 48 | # Add macos-latest and/or windows-latest if relevant for this package. 49 | os: [ubuntu-latest] 50 | sdk: [3.3, dev] 51 | steps: 52 | - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 53 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 54 | with: 55 | sdk: ${{ matrix.sdk }} 56 | - id: install 57 | name: Install dependencies 58 | run: dart pub get 59 | - name: Run VM tests 60 | run: dart test --platform vm 61 | if: always() && steps.install.outcome == 'success' 62 | - name: Run Chrome tests - js 63 | run: dart test --platform chrome 64 | if: always() && steps.install.outcome == 'success' 65 | - name: Run Chrome tests - wasm 66 | run: dart test --platform chrome --compiler dart2wasm 67 | if: always() && steps.install.outcome == 'success' && matrix.sdk == 'dev' 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .packages 2 | .pub/ 3 | .dart_tool/ 4 | build/ 5 | pubspec.lock 6 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Google Inc. 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.3.2-wip 2 | 3 | * Require Dart 3.3 4 | * Fix bug where a `flushTimers` or `elapse` call from within 5 | the callback of a periodic timer would immediately invoke 6 | the same timer. 7 | 8 | ## 1.3.1 9 | 10 | * Populate the pubspec `repository` field. 11 | 12 | ## 1.3.0 13 | 14 | * `FakeTimer.tick` will return a value instead of throwing. 15 | * `FakeAsync.includeTimerStackTrace` allows controlling whether timers created 16 | with a FakeAsync will include a creation Stack Trace. 17 | 18 | ## 1.2.0 19 | 20 | * Stable release for null safety. 21 | 22 | ## 1.2.0-nullsafety.3 23 | 24 | * Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release 25 | guidelines. 26 | 27 | ## 1.2.0-nullsafety.2 28 | 29 | * Allow prerelease versions of the 2.12 sdk. 30 | 31 | ## 1.2.0-nullsafety.1 32 | 33 | * Allow 2.10 stable and 2.11.0 dev SDK versions. 34 | 35 | ## 1.2.0-nullsafety 36 | 37 | Pre-release for the null safety migration of this package. 38 | 39 | Note that `1.2.0` may not be the final stable null safety release version, 40 | we reserve the right to release it as a `2.0.0` breaking change. 41 | 42 | This release will be pinned to only allow pre-release sdk versions starting 43 | from `2.10.0-0`. 44 | 45 | ## 1.1.0 46 | 47 | * Exposed the `FakeTimer` class as a public class. 48 | * Added `FakeAsync.pendingTimers` which gives access to all pending timers at 49 | the time of the call. 50 | 51 | ## 1.0.2 52 | 53 | * Update min SDK to 2.2.0 54 | 55 | ## 1.0.1 56 | 57 | * Update to lowercase Dart core library constants. 58 | * Fix use of deprecated `isInstanceOf` matcher. 59 | 60 | ## 1.0.0 61 | 62 | This release contains the `FakeAsync` class that was defined in [`quiver`][]. 63 | It's backwards-compatible with both the `quiver` version *and* the old version 64 | of the `fake_async` package. 65 | 66 | [`quiver`]: https://pub.dev/packages/quiver 67 | 68 | ### New Features 69 | 70 | * A top-level `fakeAsync()` function was added that encapsulates 71 | `new FakeAsync().run(...)`. 72 | 73 | ### New Features Relative to `quiver` 74 | 75 | * `FakeAsync.elapsed` returns the total amount of fake time elapsed since the 76 | `FakeAsync` instance was created. 77 | 78 | * `new FakeAsync()` now takes an `initialTime` argument that sets the default 79 | time for clocks created with `FakeAsync.getClock()`, and for the `clock` 80 | package's top-level `clock` variable. 81 | 82 | ### New Features Relative to `fake_async` 0.1 83 | 84 | * `FakeAsync.periodicTimerCount`, `FakeAsync.nonPeriodicTimerCount`, and 85 | `FakeAsync.microtaskCount` provide visibility into the events scheduled within 86 | `FakeAsync.run()`. 87 | 88 | * `FakeAsync.getClock()` provides access to fully-featured `Clock` objects based 89 | on `FakeAsync`'s elapsed time. 90 | 91 | * `FakeAsync.flushMicrotasks()` empties the microtask queue without elapsing any 92 | time or running any timers. 93 | 94 | * `FakeAsync.flushTimers()` runs all microtasks and timers until there are no 95 | more scheduled. 96 | 97 | ## 0.1.2 98 | 99 | * Integrate with the clock package. 100 | 101 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at 2 | the end). 3 | 4 | ### Before you contribute 5 | 6 | Before we can use your code, you must sign the 7 | [Google Individual Contributor License Agreement][CLA] (CLA), which you can do 8 | online. The CLA is necessary mainly because you own the copyright to your 9 | changes, even after your contribution becomes part of our codebase, so we need 10 | your permission to use and distribute your code. We also need to be sure of 11 | various other things—for instance that you'll tell us if you know that your code 12 | infringes on other people's patents. You don't have to sign the CLA until after 13 | you've submitted your code for review and a member has approved it, but you must 14 | do it before we can put your code into our codebase. 15 | 16 | Before you start working on a larger contribution, you should get in touch with 17 | us first through the issue tracker with your idea so that we can help out and 18 | possibly guide you. Coordinating up front makes it much easier to avoid 19 | frustration later on. 20 | 21 | [CLA]: https://cla.developers.google.com/about/google-individual 22 | 23 | ### Code reviews 24 | 25 | All submissions, including submissions by project members, require review. We 26 | recommend [forking the repository][fork], making changes in your fork, and 27 | [sending us a pull request][pr] so we can review the changes and merge them into 28 | this repository. 29 | 30 | [fork]: https://help.github.com/articles/about-forks/ 31 | [pr]: https://help.github.com/articles/creating-a-pull-request/ 32 | 33 | Functional changes will require tests to be added or changed. The tests live in 34 | the `test/` directory, and are run with `pub run test`. If you need to create 35 | new tests, use the existing tests as a guideline for what they should look like. 36 | 37 | Before you send your pull request, make sure all the tests pass! 38 | 39 | ### File headers 40 | 41 | When creating new source files, please copy the file header from 42 | https://github.com/dart-lang/fake_async/blob/master/lib/fake_async.dart and 43 | update the year for the copyright to the current year. 44 | 45 | ### The small print 46 | 47 | Contributions made by corporations are covered by a different agreement than the 48 | one above, the 49 | [Software Grant and Corporate Contributor License Agreement][CCLA]. 50 | 51 | [CCLA]: https://developers.google.com/open-source/cla/corporate 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > This repo has moved to https://github.com/dart-lang/test/tree/master/pkgs/fake_async 3 | 4 | [![Dart CI](https://github.com/dart-lang/fake_async/actions/workflows/test-package.yml/badge.svg)](https://github.com/dart-lang/fake_async/actions/workflows/test-package.yml) 5 | [![pub package](https://img.shields.io/pub/v/fake_async.svg)](https://pub.dev/packages/fake_async) 6 | [![package publisher](https://img.shields.io/pub/publisher/fake_async.svg)](https://pub.dev/packages/fake_async/publisher) 7 | 8 | This package provides a [`FakeAsync`][] class, which makes it easy to 9 | deterministically test code that uses asynchronous features like `Future`s, 10 | `Stream`s, `Timer`s, and microtasks. It creates an environment in which the user 11 | can explicitly control Dart's notion of the "current time". When the time is 12 | advanced, `FakeAsync` fires all asynchronous events that are scheduled for that 13 | time period without actually needing the test to wait for real time to elapse. 14 | 15 | [`FakeAsync`]: https://www.dartdocs.org/documentation/fake_async/latest/fake_async/FakeAsync-class.html 16 | 17 | For example: 18 | 19 | ```dart 20 | import 'dart:async'; 21 | 22 | import 'package:fake_async/fake_async.dart'; 23 | import 'package:test/test.dart'; 24 | 25 | void main() { 26 | test("Future.timeout() throws an error once the timeout is up", () { 27 | // Any code run within [fakeAsync] is run within the context of the 28 | // [FakeAsync] object passed to the callback. 29 | fakeAsync((async) { 30 | // All asynchronous features that rely on timing are automatically 31 | // controlled by [fakeAsync]. 32 | expect(Completer().future.timeout(Duration(seconds: 5)), 33 | throwsA(isA())); 34 | 35 | // This will cause the timeout above to fire immediately, without waiting 36 | // 5 seconds of real time. 37 | async.elapse(Duration(seconds: 5)); 38 | }); 39 | }); 40 | } 41 | ``` 42 | 43 | ## Integration With `clock` 44 | 45 | `FakeAsync` can't control the time reported by [`DateTime.now()`][] or by 46 | the [`Stopwatch`][] class, since they're not part of `dart:async`. However, if 47 | you create them using the [`clock`][] package's [`clock.now()`][] or 48 | [`clock.stopwatch()`][] functions, `FakeAsync` will automatically override 49 | them to use the same notion of time as `dart:async` classes. 50 | 51 | [`DateTime.now()`]: https://api.dart.dev/stable/dart-core/DateTime/DateTime.now.html 52 | [`Stopwatch`]: https://api.dart.dev/stable/dart-core/Stopwatch-class.html 53 | [`clock`]: https://pub.dev/packages/clock 54 | [`clock.now()`]: https://pub.dev/documentation/clock/latest/clock/Clock/now.html 55 | [`clock.stopwatch()`]: https://pub.dev/documentation/clock/latest/clock/Clock/stopwatch.html 56 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:dart_flutter_team_lints/analysis_options.yaml 2 | analyzer: 3 | language: 4 | strict-casts: true 5 | 6 | linter: 7 | rules: 8 | - avoid_bool_literals_in_conditional_expressions 9 | - avoid_classes_with_only_static_members 10 | - avoid_returning_this 11 | - avoid_unused_constructor_parameters 12 | - cancel_subscriptions 13 | - cascade_invocations 14 | - comment_references 15 | - join_return_with_assignment 16 | - literal_only_boolean_expressions 17 | - no_adjacent_strings_in_list 18 | - package_api_docs 19 | - prefer_const_constructors 20 | - prefer_final_locals 21 | - test_types_in_equals 22 | - unnecessary_await_in_return 23 | -------------------------------------------------------------------------------- /lib/fake_async.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | import 'dart:collection'; 17 | 18 | import 'package:clock/clock.dart'; 19 | import 'package:collection/collection.dart'; 20 | 21 | /// The type of a microtask callback. 22 | typedef _Microtask = void Function(); 23 | 24 | /// Runs [callback] in a [Zone] where all asynchrony is controlled by an 25 | /// instance of [FakeAsync]. 26 | /// 27 | /// All [Future]s, [Stream]s, [Timer]s, microtasks, and other time-based 28 | /// asynchronous features used within [callback] are controlled by calls to 29 | /// [FakeAsync.elapse] rather than the passing of real time. 30 | /// 31 | /// The [`clock`][] property will be set to a clock that reports the fake 32 | /// elapsed time. By default, it starts at the time [fakeAsync] was created 33 | /// (according to [`clock.now()`][]), but this can be controlled by passing 34 | /// [initialTime]. 35 | /// 36 | /// [`clock`]: https://www.dartdocs.org/documentation/clock/latest/clock/clock.html 37 | /// [`clock.now()`]: https://www.dartdocs.org/documentation/clock/latest/clock/Clock/now.html 38 | /// 39 | /// Returns the result of [callback]. 40 | T fakeAsync(T Function(FakeAsync async) callback, {DateTime? initialTime}) => 41 | FakeAsync(initialTime: initialTime).run(callback); 42 | 43 | /// A class that mocks out the passage of time within a [Zone]. 44 | /// 45 | /// Test code can be passed as a callback to [run], which causes it to be run in 46 | /// a [Zone] which fakes timer and microtask creation, such that they are run 47 | /// during calls to [elapse] which simulates the asynchronous passage of time. 48 | /// 49 | /// The synchronous passage of time (as from blocking or expensive calls) can 50 | /// also be simulated using [elapseBlocking]. 51 | class FakeAsync { 52 | /// The value of [clock] within [run]. 53 | late final Clock _clock; 54 | 55 | /// The amount of fake time that's elapsed since this [FakeAsync] was 56 | /// created. 57 | Duration get elapsed => _elapsed; 58 | var _elapsed = Duration.zero; 59 | 60 | /// Whether Timers created by this FakeAsync will include a creation stack 61 | /// trace in [FakeAsync.pendingTimersDebugString]. 62 | final bool includeTimerStackTrace; 63 | 64 | /// The fake time at which the current call to [elapse] will finish running. 65 | /// 66 | /// This is `null` if there's no current call to [elapse]. 67 | Duration? _elapsingTo; 68 | 69 | /// Tasks that are scheduled to run when fake time progresses. 70 | final _microtasks = Queue<_Microtask>(); 71 | 72 | /// All timers created within [run]. 73 | final _timers = {}; 74 | 75 | /// All the current pending timers. 76 | List get pendingTimers => _timers.toList(growable: false); 77 | 78 | /// The debug strings for all the current pending timers. 79 | List get pendingTimersDebugString => 80 | pendingTimers.map((timer) => timer.debugString).toList(growable: false); 81 | 82 | /// The number of active periodic timers created within a call to [run] or 83 | /// [fakeAsync]. 84 | int get periodicTimerCount => 85 | _timers.where((timer) => timer.isPeriodic).length; 86 | 87 | /// The number of active non-periodic timers created within a call to [run] or 88 | /// [fakeAsync]. 89 | int get nonPeriodicTimerCount => 90 | _timers.where((timer) => !timer.isPeriodic).length; 91 | 92 | /// The number of pending microtasks scheduled within a call to [run] or 93 | /// [fakeAsync]. 94 | int get microtaskCount => _microtasks.length; 95 | 96 | /// Creates a [FakeAsync]. 97 | /// 98 | /// Within [run], the [`clock`][] property will start at [initialTime] and 99 | /// move forward as fake time elapses. 100 | /// 101 | /// [`clock`]: https://www.dartdocs.org/documentation/clock/latest/clock/clock.html 102 | /// 103 | /// Note: it's usually more convenient to use [fakeAsync] rather than creating 104 | /// a [FakeAsync] object and calling [run] manually. 105 | FakeAsync({DateTime? initialTime, this.includeTimerStackTrace = true}) { 106 | final nonNullInitialTime = initialTime ?? clock.now(); 107 | _clock = Clock(() => nonNullInitialTime.add(elapsed)); 108 | } 109 | 110 | /// Returns a fake [Clock] whose time can is elapsed by calls to [elapse] and 111 | /// [elapseBlocking]. 112 | /// 113 | /// The returned clock starts at [initialTime] plus the fake time that's 114 | /// already been elapsed. Further calls to [elapse] and [elapseBlocking] will 115 | /// advance the clock as well. 116 | /// 117 | /// Note that it's usually easier to use the top-level [`clock`][] property. 118 | /// Only call this function if you want a different [initialTime] than the 119 | /// default. 120 | /// 121 | /// [`clock`]: https://www.dartdocs.org/documentation/clock/latest/clock/clock.html 122 | Clock getClock(DateTime initialTime) => 123 | Clock(() => initialTime.add(_elapsed)); 124 | 125 | /// Simulates the asynchronous passage of time. 126 | /// 127 | /// Throws an [ArgumentError] if [duration] is negative. Throws a [StateError] 128 | /// if a previous call to [elapse] has not yet completed. 129 | /// 130 | /// Any timers created within [run] or [fakeAsync] will fire if their time is 131 | /// within [duration]. The microtask queue is processed before and after each 132 | /// timer fires. 133 | void elapse(Duration duration) { 134 | if (duration.inMicroseconds < 0) { 135 | throw ArgumentError.value(duration, 'duration', 'may not be negative'); 136 | } else if (_elapsingTo != null) { 137 | throw StateError('Cannot elapse until previous elapse is complete.'); 138 | } 139 | 140 | _elapsingTo = _elapsed + duration; 141 | _fireTimersWhile((next) => next._nextCall <= _elapsingTo!); 142 | _elapseTo(_elapsingTo!); 143 | _elapsingTo = null; 144 | } 145 | 146 | /// Simulates the synchronous passage of time, resulting from blocking or 147 | /// expensive calls. 148 | /// 149 | /// Neither timers nor microtasks are run during this call, but if this is 150 | /// called within [elapse] they may fire afterwards. 151 | /// 152 | /// Throws an [ArgumentError] if [duration] is negative. 153 | void elapseBlocking(Duration duration) { 154 | if (duration.inMicroseconds < 0) { 155 | throw ArgumentError('Cannot call elapse with negative duration'); 156 | } 157 | 158 | _elapsed += duration; 159 | final elapsingTo = _elapsingTo; 160 | if (elapsingTo != null && _elapsed > elapsingTo) _elapsingTo = _elapsed; 161 | } 162 | 163 | /// Runs [callback] in a [Zone] where all asynchrony is controlled by `this`. 164 | /// 165 | /// All [Future]s, [Stream]s, [Timer]s, microtasks, and other time-based 166 | /// asynchronous features used within [callback] are controlled by calls to 167 | /// [elapse] rather than the passing of real time. 168 | /// 169 | /// The [`clock`][] property will be set to a clock that reports the fake 170 | /// elapsed time. By default, it starts at the time the [FakeAsync] was 171 | /// created (according to [`clock.now()`][]), but this can be controlled by 172 | /// passing `initialTime` to [FakeAsync.new]. 173 | /// 174 | /// [`clock`]: https://www.dartdocs.org/documentation/clock/latest/clock/clock.html 175 | /// [`clock.now()`]: https://www.dartdocs.org/documentation/clock/latest/clock/Clock/now.html 176 | /// 177 | /// Calls [callback] with `this` as argument and returns its result. 178 | /// 179 | /// Note: it's usually more convenient to use [fakeAsync] rather than creating 180 | /// a [FakeAsync] object and calling [run] manually. 181 | T run(T Function(FakeAsync self) callback) => 182 | runZoned(() => withClock(_clock, () => callback(this)), 183 | zoneSpecification: ZoneSpecification( 184 | createTimer: (_, __, ___, duration, callback) => 185 | _createTimer(duration, callback, false), 186 | createPeriodicTimer: (_, __, ___, duration, callback) => 187 | _createTimer(duration, callback, true), 188 | scheduleMicrotask: (_, __, ___, microtask) => 189 | _microtasks.add(microtask))); 190 | 191 | /// Runs all pending microtasks scheduled within a call to [run] or 192 | /// [fakeAsync] until there are no more microtasks scheduled. 193 | /// 194 | /// Does not run timers. 195 | void flushMicrotasks() { 196 | while (_microtasks.isNotEmpty) { 197 | _microtasks.removeFirst()(); 198 | } 199 | } 200 | 201 | /// Elapses time until there are no more active timers. 202 | /// 203 | /// If `flushPeriodicTimers` is `true` (the default), this will repeatedly run 204 | /// periodic timers until they're explicitly canceled. Otherwise, this will 205 | /// stop when the only active timers are periodic. 206 | /// 207 | /// The [timeout] controls how much fake time may elapse before a [StateError] 208 | /// is thrown. This ensures that a periodic timer doesn't cause this method to 209 | /// deadlock. It defaults to one hour. 210 | void flushTimers( 211 | {Duration timeout = const Duration(hours: 1), 212 | bool flushPeriodicTimers = true}) { 213 | final absoluteTimeout = _elapsed + timeout; 214 | _fireTimersWhile((timer) { 215 | if (timer._nextCall > absoluteTimeout) { 216 | // TODO(nweiz): Make this a [TimeoutException]. 217 | throw StateError('Exceeded timeout $timeout while flushing timers'); 218 | } 219 | 220 | if (flushPeriodicTimers) return _timers.isNotEmpty; 221 | 222 | // Continue firing timers until the only ones left are periodic *and* 223 | // every periodic timer has had a change to run against the final 224 | // value of [_elapsed]. 225 | return _timers 226 | .any((timer) => !timer.isPeriodic || timer._nextCall <= _elapsed); 227 | }); 228 | } 229 | 230 | /// Invoke the callback for each timer until [predicate] returns `false` for 231 | /// the next timer that would be fired. 232 | /// 233 | /// Microtasks are flushed before and after each timer is fired. Before each 234 | /// timer fires, [_elapsed] is updated to the appropriate duration. 235 | void _fireTimersWhile(bool Function(FakeTimer timer) predicate) { 236 | flushMicrotasks(); 237 | for (;;) { 238 | if (_timers.isEmpty) break; 239 | 240 | final timer = minBy(_timers, (FakeTimer timer) => timer._nextCall)!; 241 | if (!predicate(timer)) break; 242 | 243 | _elapseTo(timer._nextCall); 244 | timer._fire(); 245 | flushMicrotasks(); 246 | } 247 | } 248 | 249 | /// Creates a new timer controlled by `this` that fires [callback] after 250 | /// [duration] (or every [duration] if [periodic] is `true`). 251 | Timer _createTimer(Duration duration, Function callback, bool periodic) { 252 | final timer = FakeTimer._(duration, callback, periodic, this, 253 | includeStackTrace: includeTimerStackTrace); 254 | _timers.add(timer); 255 | return timer; 256 | } 257 | 258 | /// Sets [_elapsed] to [to] if [to] is longer than [_elapsed]. 259 | void _elapseTo(Duration to) { 260 | if (to > _elapsed) _elapsed = to; 261 | } 262 | } 263 | 264 | /// An implementation of [Timer] that's controlled by a [FakeAsync]. 265 | class FakeTimer implements Timer { 266 | /// If this is periodic, the time that should elapse between firings of this 267 | /// timer. 268 | /// 269 | /// This is not used by non-periodic timers. 270 | final Duration duration; 271 | 272 | /// The callback to invoke when the timer fires. 273 | /// 274 | /// For periodic timers, this is a `void Function(Timer)`. For non-periodic 275 | /// timers, it's a `void Function()`. 276 | final Function _callback; 277 | 278 | /// Whether this is a periodic timer. 279 | final bool isPeriodic; 280 | 281 | /// The [FakeAsync] instance that controls this timer. 282 | final FakeAsync _async; 283 | 284 | /// The value of [FakeAsync._elapsed] at (or after) which this timer should be 285 | /// fired. 286 | late Duration _nextCall; 287 | 288 | /// The current stack trace when this timer was created. 289 | /// 290 | /// If [FakeAsync.includeTimerStackTrace] is set to false then accessing 291 | /// this field will throw a [TypeError]. 292 | StackTrace get creationStackTrace => _creationStackTrace!; 293 | final StackTrace? _creationStackTrace; 294 | 295 | var _tick = 0; 296 | 297 | @override 298 | int get tick => _tick; 299 | 300 | /// Returns debugging information to try to identify the source of the 301 | /// [Timer]. 302 | String get debugString => 'Timer (duration: $duration, periodic: $isPeriodic)' 303 | '${_creationStackTrace != null ? ', created:\n$creationStackTrace' : ''}'; 304 | 305 | FakeTimer._(Duration duration, this._callback, this.isPeriodic, this._async, 306 | {bool includeStackTrace = true}) 307 | : duration = duration < Duration.zero ? Duration.zero : duration, 308 | _creationStackTrace = includeStackTrace ? StackTrace.current : null { 309 | _nextCall = _async._elapsed + this.duration; 310 | } 311 | 312 | @override 313 | bool get isActive => _async._timers.contains(this); 314 | 315 | @override 316 | void cancel() => _async._timers.remove(this); 317 | 318 | /// Fires this timer's callback and updates its state as necessary. 319 | void _fire() { 320 | assert(isActive); 321 | _tick++; 322 | if (isPeriodic) { 323 | _nextCall += duration; 324 | // ignore: avoid_dynamic_calls 325 | _callback(this); 326 | } else { 327 | cancel(); 328 | // ignore: avoid_dynamic_calls 329 | _callback(); 330 | } 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: fake_async 2 | version: 1.3.2-wip 3 | description: >- 4 | Fake asynchronous events such as timers and microtasks for deterministic 5 | testing. 6 | repository: https://github.com/dart-lang/fake_async 7 | 8 | environment: 9 | sdk: ^3.3.0 10 | 11 | dependencies: 12 | clock: ^1.1.0 13 | collection: ^1.15.0 14 | 15 | dev_dependencies: 16 | async: ^2.5.0 17 | dart_flutter_team_lints: ^2.0.0 18 | test: ^1.16.0 19 | -------------------------------------------------------------------------------- /test/fake_async_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | 17 | import 'package:clock/clock.dart'; 18 | import 'package:fake_async/fake_async.dart'; 19 | import 'package:test/test.dart'; 20 | 21 | void main() { 22 | final initialTime = DateTime(2000); 23 | final elapseBy = const Duration(days: 1); 24 | 25 | test('should set initial time', () { 26 | expect(FakeAsync().getClock(initialTime).now(), initialTime); 27 | }); 28 | 29 | group('elapseBlocking', () { 30 | test('should elapse time without calling timers', () { 31 | Timer(elapseBy ~/ 2, neverCalled); 32 | FakeAsync().elapseBlocking(elapseBy); 33 | }); 34 | 35 | test('should elapse time by the specified amount', () { 36 | final async = FakeAsync()..elapseBlocking(elapseBy); 37 | expect(async.elapsed, elapseBy); 38 | }); 39 | 40 | test('should throw when called with a negative duration', () { 41 | expect(() => FakeAsync().elapseBlocking(const Duration(days: -1)), 42 | throwsArgumentError); 43 | }); 44 | }); 45 | 46 | group('elapse', () { 47 | test('should elapse time by the specified amount', () { 48 | FakeAsync().run((async) { 49 | async.elapse(elapseBy); 50 | expect(async.elapsed, elapseBy); 51 | }); 52 | }); 53 | 54 | test('should throw ArgumentError when called with a negative duration', () { 55 | expect(() => FakeAsync().elapse(const Duration(days: -1)), 56 | throwsArgumentError); 57 | }); 58 | 59 | test('should throw when called before previous call is complete', () { 60 | FakeAsync().run((async) { 61 | Timer(elapseBy ~/ 2, expectAsync0(() { 62 | expect(() => async.elapse(elapseBy), throwsStateError); 63 | })); 64 | async.elapse(elapseBy); 65 | }); 66 | }); 67 | 68 | group('when creating timers', () { 69 | test('should call timers expiring before or at end time', () { 70 | FakeAsync().run((async) { 71 | Timer(elapseBy ~/ 2, expectAsync0(() {})); 72 | Timer(elapseBy, expectAsync0(() {})); 73 | async.elapse(elapseBy); 74 | }); 75 | }); 76 | 77 | test('should call timers expiring due to elapseBlocking', () { 78 | FakeAsync().run((async) { 79 | Timer(elapseBy, () => async.elapseBlocking(elapseBy)); 80 | Timer(elapseBy * 2, expectAsync0(() {})); 81 | async.elapse(elapseBy); 82 | expect(async.elapsed, elapseBy * 2); 83 | }); 84 | }); 85 | 86 | test('should call timers at their scheduled time', () { 87 | FakeAsync().run((async) { 88 | Timer(elapseBy ~/ 2, expectAsync0(() { 89 | expect(async.elapsed, elapseBy ~/ 2); 90 | })); 91 | 92 | final periodicCalledAt = []; 93 | Timer.periodic( 94 | elapseBy ~/ 2, (_) => periodicCalledAt.add(async.elapsed)); 95 | 96 | async.elapse(elapseBy); 97 | expect(periodicCalledAt, [elapseBy ~/ 2, elapseBy]); 98 | }); 99 | }); 100 | 101 | test('should not call timers expiring after end time', () { 102 | FakeAsync().run((async) { 103 | Timer(elapseBy * 2, neverCalled); 104 | async.elapse(elapseBy); 105 | }); 106 | }); 107 | 108 | test('should not call canceled timers', () { 109 | FakeAsync().run((async) { 110 | Timer(elapseBy ~/ 2, neverCalled).cancel(); 111 | async.elapse(elapseBy); 112 | }); 113 | }); 114 | 115 | test('should call periodic timers each time the duration elapses', () { 116 | FakeAsync().run((async) { 117 | Timer.periodic(elapseBy ~/ 10, expectAsync1((_) {}, count: 10)); 118 | async.elapse(elapseBy); 119 | }); 120 | }); 121 | 122 | test('should call timers occurring at the same time in FIFO order', () { 123 | FakeAsync().run((async) { 124 | final log = []; 125 | Timer(elapseBy ~/ 2, () => log.add('1')); 126 | Timer(elapseBy ~/ 2, () => log.add('2')); 127 | async.elapse(elapseBy); 128 | expect(log, ['1', '2']); 129 | }); 130 | }); 131 | 132 | test('should maintain FIFO order even with periodic timers', () { 133 | FakeAsync().run((async) { 134 | final log = []; 135 | Timer.periodic(elapseBy ~/ 2, (_) => log.add('periodic 1')); 136 | Timer(elapseBy ~/ 2, () => log.add('delayed 1')); 137 | Timer(elapseBy, () => log.add('delayed 2')); 138 | Timer.periodic(elapseBy, (_) => log.add('periodic 2')); 139 | 140 | async.elapse(elapseBy); 141 | expect(log, [ 142 | 'periodic 1', 143 | 'delayed 1', 144 | 'periodic 1', 145 | 'delayed 2', 146 | 'periodic 2' 147 | ]); 148 | }); 149 | }); 150 | 151 | test('should process microtasks surrounding each timer', () { 152 | FakeAsync().run((async) { 153 | var microtaskCalls = 0; 154 | var timerCalls = 0; 155 | void scheduleMicrotasks() { 156 | for (var i = 0; i < 5; i++) { 157 | scheduleMicrotask(() => microtaskCalls++); 158 | } 159 | } 160 | 161 | scheduleMicrotasks(); 162 | Timer.periodic(elapseBy ~/ 5, (_) { 163 | timerCalls++; 164 | expect(microtaskCalls, 5 * timerCalls); 165 | scheduleMicrotasks(); 166 | }); 167 | async.elapse(elapseBy); 168 | expect(timerCalls, 5); 169 | expect(microtaskCalls, 5 * (timerCalls + 1)); 170 | }); 171 | }); 172 | 173 | test('should pass the periodic timer itself to callbacks', () { 174 | FakeAsync().run((async) { 175 | late Timer constructed; 176 | constructed = Timer.periodic(elapseBy, expectAsync1((passed) { 177 | expect(passed, same(constructed)); 178 | })); 179 | async.elapse(elapseBy); 180 | }); 181 | }); 182 | 183 | test('should call microtasks before advancing time', () { 184 | FakeAsync().run((async) { 185 | scheduleMicrotask(expectAsync0(() { 186 | expect(async.elapsed, Duration.zero); 187 | })); 188 | async.elapse(const Duration(minutes: 1)); 189 | }); 190 | }); 191 | 192 | test('should add event before advancing time', () { 193 | FakeAsync().run((async) { 194 | final controller = StreamController(); 195 | expect(controller.stream.first.then((_) { 196 | expect(async.elapsed, Duration.zero); 197 | }), completes); 198 | controller.add(null); 199 | async.elapse(const Duration(minutes: 1)); 200 | }); 201 | }); 202 | 203 | test('should increase negative duration timers to zero duration', () { 204 | FakeAsync().run((async) { 205 | final negativeDuration = const Duration(days: -1); 206 | Timer(negativeDuration, expectAsync0(() { 207 | expect(async.elapsed, Duration.zero); 208 | })); 209 | async.elapse(const Duration(minutes: 1)); 210 | }); 211 | }); 212 | 213 | test('should not be additive with elapseBlocking', () { 214 | FakeAsync().run((async) { 215 | Timer(Duration.zero, () => async.elapseBlocking(elapseBy * 5)); 216 | async.elapse(elapseBy); 217 | expect(async.elapsed, elapseBy * 5); 218 | }); 219 | }); 220 | 221 | group('isActive', () { 222 | test('should be false after timer is run', () { 223 | FakeAsync().run((async) { 224 | final timer = Timer(elapseBy ~/ 2, () {}); 225 | async.elapse(elapseBy); 226 | expect(timer.isActive, isFalse); 227 | }); 228 | }); 229 | 230 | test('should be true after periodic timer is run', () { 231 | FakeAsync().run((async) { 232 | final timer = Timer.periodic(elapseBy ~/ 2, (_) {}); 233 | async.elapse(elapseBy); 234 | expect(timer.isActive, isTrue); 235 | }); 236 | }); 237 | 238 | test('should be false after timer is canceled', () { 239 | FakeAsync().run((async) { 240 | final timer = Timer(elapseBy ~/ 2, () {})..cancel(); 241 | expect(timer.isActive, isFalse); 242 | }); 243 | }); 244 | }); 245 | 246 | test('should work with new Future()', () { 247 | FakeAsync().run((async) { 248 | Future(expectAsync0(() {})); 249 | async.elapse(Duration.zero); 250 | }); 251 | }); 252 | 253 | test('should work with Future.delayed', () { 254 | FakeAsync().run((async) { 255 | Future.delayed(elapseBy, expectAsync0(() {})); 256 | async.elapse(elapseBy); 257 | }); 258 | }); 259 | 260 | test('should work with Future.timeout', () { 261 | FakeAsync().run((async) { 262 | final completer = Completer(); 263 | expect(completer.future.timeout(elapseBy ~/ 2), 264 | throwsA(const TypeMatcher())); 265 | async.elapse(elapseBy); 266 | completer.complete(); 267 | }); 268 | }); 269 | 270 | // TODO: Pausing and resuming the timeout Stream doesn't work since 271 | // it uses `new Stopwatch()`. 272 | // 273 | // See https://code.google.com/p/dart/issues/detail?id=18149 274 | test('should work with Stream.periodic', () { 275 | FakeAsync().run((async) { 276 | expect(Stream.periodic(const Duration(minutes: 1), (i) => i), 277 | emitsInOrder([0, 1, 2])); 278 | async.elapse(const Duration(minutes: 3)); 279 | }); 280 | }); 281 | 282 | test('should work with Stream.timeout', () { 283 | FakeAsync().run((async) { 284 | final controller = StreamController(); 285 | final timed = controller.stream.timeout(const Duration(minutes: 2)); 286 | 287 | final events = []; 288 | final errors = []; 289 | timed.listen(events.add, onError: errors.add); 290 | 291 | controller.add(0); 292 | async.elapse(const Duration(minutes: 1)); 293 | expect(events, [0]); 294 | 295 | async.elapse(const Duration(minutes: 1)); 296 | expect(errors, hasLength(1)); 297 | expect(errors.first, const TypeMatcher()); 298 | }); 299 | }); 300 | }); 301 | }); 302 | 303 | group('flushMicrotasks', () { 304 | test('should flush a microtask', () { 305 | FakeAsync().run((async) { 306 | Future.microtask(expectAsync0(() {})); 307 | async.flushMicrotasks(); 308 | }); 309 | }); 310 | 311 | test('should flush microtasks scheduled by microtasks in order', () { 312 | FakeAsync().run((async) { 313 | final log = []; 314 | scheduleMicrotask(() { 315 | log.add(1); 316 | scheduleMicrotask(() => log.add(3)); 317 | }); 318 | scheduleMicrotask(() => log.add(2)); 319 | 320 | async.flushMicrotasks(); 321 | expect(log, [1, 2, 3]); 322 | }); 323 | }); 324 | 325 | test('should not run timers', () { 326 | FakeAsync().run((async) { 327 | final log = []; 328 | scheduleMicrotask(() => log.add(1)); 329 | Timer.run(() => log.add(2)); 330 | Timer.periodic(const Duration(seconds: 1), (_) => log.add(2)); 331 | 332 | async.flushMicrotasks(); 333 | expect(log, [1]); 334 | }); 335 | }); 336 | }); 337 | 338 | group('flushTimers', () { 339 | test('should flush timers in FIFO order', () { 340 | FakeAsync().run((async) { 341 | final log = []; 342 | Timer.run(() { 343 | log.add(1); 344 | Timer(elapseBy, () => log.add(3)); 345 | }); 346 | Timer.run(() => log.add(2)); 347 | 348 | async.flushTimers(timeout: elapseBy * 2); 349 | expect(log, [1, 2, 3]); 350 | expect(async.elapsed, elapseBy); 351 | }); 352 | }); 353 | 354 | test( 355 | 'should run collateral periodic timers with non-periodic first if ' 356 | 'scheduled first', () { 357 | FakeAsync().run((async) { 358 | final log = []; 359 | Timer(const Duration(seconds: 2), () => log.add('delayed')); 360 | Timer.periodic(const Duration(seconds: 1), (_) => log.add('periodic')); 361 | 362 | async.flushTimers(flushPeriodicTimers: false); 363 | expect(log, ['periodic', 'delayed', 'periodic']); 364 | }); 365 | }); 366 | 367 | test( 368 | 'should run collateral periodic timers with periodic first ' 369 | 'if scheduled first', () { 370 | FakeAsync().run((async) { 371 | final log = []; 372 | Timer.periodic(const Duration(seconds: 1), (_) => log.add('periodic')); 373 | Timer(const Duration(seconds: 2), () => log.add('delayed')); 374 | 375 | async.flushTimers(flushPeriodicTimers: false); 376 | expect(log, ['periodic', 'periodic', 'delayed']); 377 | }); 378 | }); 379 | 380 | test('should time out', () { 381 | FakeAsync().run((async) { 382 | // Schedule 3 timers. All but the last one should fire. 383 | for (var delay in [30, 60, 90]) { 384 | Timer(Duration(minutes: delay), 385 | expectAsync0(() {}, count: delay == 90 ? 0 : 1)); 386 | } 387 | 388 | expect(() => async.flushTimers(), throwsStateError); 389 | }); 390 | }); 391 | 392 | test('should time out a chain of timers', () { 393 | FakeAsync().run((async) { 394 | var count = 0; 395 | void createTimer() { 396 | Timer(const Duration(minutes: 30), () { 397 | count++; 398 | createTimer(); 399 | }); 400 | } 401 | 402 | createTimer(); 403 | expect(() => async.flushTimers(timeout: const Duration(hours: 2)), 404 | throwsStateError); 405 | expect(count, 4); 406 | }); 407 | }); 408 | 409 | test('should time out periodic timers', () { 410 | FakeAsync().run((async) { 411 | Timer.periodic( 412 | const Duration(minutes: 30), expectAsync1((_) {}, count: 2)); 413 | expect(() => async.flushTimers(timeout: const Duration(hours: 1)), 414 | throwsStateError); 415 | }); 416 | }); 417 | 418 | test('should flush periodic timers', () { 419 | FakeAsync().run((async) { 420 | var count = 0; 421 | Timer.periodic(const Duration(minutes: 30), (timer) { 422 | if (count == 3) timer.cancel(); 423 | count++; 424 | }); 425 | async.flushTimers(timeout: const Duration(hours: 20)); 426 | expect(count, 4); 427 | }); 428 | }); 429 | 430 | test('should compute absolute timeout as elapsed + timeout', () { 431 | FakeAsync().run((async) { 432 | var count = 0; 433 | void createTimer() { 434 | Timer(const Duration(minutes: 30), () { 435 | count++; 436 | if (count < 4) createTimer(); 437 | }); 438 | } 439 | 440 | createTimer(); 441 | async 442 | ..elapse(const Duration(hours: 1)) 443 | ..flushTimers(timeout: const Duration(hours: 1)); 444 | expect(count, 4); 445 | }); 446 | }); 447 | }); 448 | 449 | group('stats', () { 450 | test('should report the number of pending microtasks', () { 451 | FakeAsync().run((async) { 452 | expect(async.microtaskCount, 0); 453 | scheduleMicrotask(() {}); 454 | expect(async.microtaskCount, 1); 455 | scheduleMicrotask(() {}); 456 | expect(async.microtaskCount, 2); 457 | async.flushMicrotasks(); 458 | expect(async.microtaskCount, 0); 459 | }); 460 | }); 461 | 462 | test('it should report the number of pending periodic timers', () { 463 | FakeAsync().run((async) { 464 | expect(async.periodicTimerCount, 0); 465 | final timer = Timer.periodic(const Duration(minutes: 30), (_) {}); 466 | expect(async.periodicTimerCount, 1); 467 | Timer.periodic(const Duration(minutes: 20), (_) {}); 468 | expect(async.periodicTimerCount, 2); 469 | async.elapse(const Duration(minutes: 20)); 470 | expect(async.periodicTimerCount, 2); 471 | timer.cancel(); 472 | expect(async.periodicTimerCount, 1); 473 | }); 474 | }); 475 | 476 | test('it should report the number of pending non periodic timers', () { 477 | FakeAsync().run((async) { 478 | expect(async.nonPeriodicTimerCount, 0); 479 | final timer = Timer(const Duration(minutes: 30), () {}); 480 | expect(async.nonPeriodicTimerCount, 1); 481 | Timer(const Duration(minutes: 20), () {}); 482 | expect(async.nonPeriodicTimerCount, 2); 483 | async.elapse(const Duration(minutes: 25)); 484 | expect(async.nonPeriodicTimerCount, 1); 485 | timer.cancel(); 486 | expect(async.nonPeriodicTimerCount, 0); 487 | }); 488 | }); 489 | 490 | test('should report debugging information of pending timers', () { 491 | FakeAsync().run((fakeAsync) { 492 | expect(fakeAsync.pendingTimers, isEmpty); 493 | final nonPeriodic = 494 | Timer(const Duration(seconds: 1), () {}) as FakeTimer; 495 | final periodic = 496 | Timer.periodic(const Duration(seconds: 2), (Timer timer) {}) 497 | as FakeTimer; 498 | final debugInfo = fakeAsync.pendingTimers; 499 | expect(debugInfo.length, 2); 500 | expect( 501 | debugInfo, 502 | containsAll([ 503 | nonPeriodic, 504 | periodic, 505 | ]), 506 | ); 507 | 508 | const thisFileName = 'fake_async_test.dart'; 509 | expect(nonPeriodic.debugString, contains(':01.0')); 510 | expect(nonPeriodic.debugString, contains('periodic: false')); 511 | expect(nonPeriodic.debugString, contains(thisFileName)); 512 | expect(periodic.debugString, contains(':02.0')); 513 | expect(periodic.debugString, contains('periodic: true')); 514 | expect(periodic.debugString, contains(thisFileName)); 515 | }); 516 | }); 517 | 518 | test( 519 | 'should report debugging information of pending timers excluding ' 520 | 'stack traces', () { 521 | FakeAsync(includeTimerStackTrace: false).run((fakeAsync) { 522 | expect(fakeAsync.pendingTimers, isEmpty); 523 | final nonPeriodic = 524 | Timer(const Duration(seconds: 1), () {}) as FakeTimer; 525 | final periodic = 526 | Timer.periodic(const Duration(seconds: 2), (Timer timer) {}) 527 | as FakeTimer; 528 | final debugInfo = fakeAsync.pendingTimers; 529 | expect(debugInfo.length, 2); 530 | expect( 531 | debugInfo, 532 | containsAll([ 533 | nonPeriodic, 534 | periodic, 535 | ]), 536 | ); 537 | 538 | const thisFileName = 'fake_async_test.dart'; 539 | expect(nonPeriodic.debugString, contains(':01.0')); 540 | expect(nonPeriodic.debugString, contains('periodic: false')); 541 | expect(nonPeriodic.debugString, isNot(contains(thisFileName))); 542 | expect(periodic.debugString, contains(':02.0')); 543 | expect(periodic.debugString, contains('periodic: true')); 544 | expect(periodic.debugString, isNot(contains(thisFileName))); 545 | }); 546 | }); 547 | }); 548 | 549 | group('timers', () { 550 | test("should become inactive as soon as they're invoked", () { 551 | return FakeAsync().run((async) { 552 | late Timer timer; 553 | timer = Timer(elapseBy, expectAsync0(() { 554 | expect(timer.isActive, isFalse); 555 | })); 556 | 557 | expect(timer.isActive, isTrue); 558 | async.elapse(elapseBy); 559 | expect(timer.isActive, isFalse); 560 | }); 561 | }); 562 | 563 | test('should increment tick in a non-periodic timer', () { 564 | return FakeAsync().run((async) { 565 | late Timer timer; 566 | timer = Timer(elapseBy, expectAsync0(() { 567 | expect(timer.tick, 1); 568 | })); 569 | 570 | expect(timer.tick, 0); 571 | async.elapse(elapseBy); 572 | }); 573 | }); 574 | 575 | test('should increment tick in a periodic timer', () { 576 | return FakeAsync().run((async) { 577 | final ticks = []; 578 | Timer.periodic( 579 | elapseBy, 580 | expectAsync1((timer) { 581 | ticks.add(timer.tick); 582 | }, count: 2)); 583 | async 584 | ..elapse(elapseBy) 585 | ..elapse(elapseBy); 586 | expect(ticks, [1, 2]); 587 | }); 588 | }); 589 | 590 | test('should update periodic timer state before invoking callback', () { 591 | // Regression test for: https://github.com/dart-lang/fake_async/issues/88 592 | FakeAsync().run((async) { 593 | final log = []; 594 | Timer.periodic(const Duration(seconds: 2), (timer) { 595 | log.add('periodic ${timer.tick}'); 596 | async.elapse(Duration.zero); 597 | }); 598 | Timer(const Duration(seconds: 3), () { 599 | log.add('single'); 600 | }); 601 | 602 | async.flushTimers(flushPeriodicTimers: false); 603 | expect(log, ['periodic 1', 'single']); 604 | }); 605 | }); 606 | }); 607 | 608 | group('clock', () { 609 | test('updates following elapse()', () { 610 | FakeAsync().run((async) { 611 | final before = clock.now(); 612 | async.elapse(elapseBy); 613 | expect(clock.now(), before.add(elapseBy)); 614 | }); 615 | }); 616 | 617 | test('updates following elapseBlocking()', () { 618 | FakeAsync().run((async) { 619 | final before = clock.now(); 620 | async.elapseBlocking(elapseBy); 621 | expect(clock.now(), before.add(elapseBy)); 622 | }); 623 | }); 624 | 625 | group('starts at', () { 626 | test('the time at which the FakeAsync was created', () { 627 | final start = DateTime.now(); 628 | FakeAsync().run((async) { 629 | expect(clock.now(), _closeToTime(start)); 630 | async.elapse(elapseBy); 631 | expect(clock.now(), _closeToTime(start.add(elapseBy))); 632 | }); 633 | }); 634 | 635 | test('the value of clock.now()', () { 636 | final start = DateTime(1990, 8, 11); 637 | withClock(Clock.fixed(start), () { 638 | FakeAsync().run((async) { 639 | expect(clock.now(), start); 640 | async.elapse(elapseBy); 641 | expect(clock.now(), start.add(elapseBy)); 642 | }); 643 | }); 644 | }); 645 | 646 | test('an explicit value', () { 647 | final start = DateTime(1990, 8, 11); 648 | FakeAsync(initialTime: start).run((async) { 649 | expect(clock.now(), start); 650 | async.elapse(elapseBy); 651 | expect(clock.now(), start.add(elapseBy)); 652 | }); 653 | }); 654 | }); 655 | }); 656 | } 657 | 658 | /// Returns a matcher that asserts that a [DateTime] is within 100ms of 659 | /// [expected]. 660 | Matcher _closeToTime(DateTime expected) => predicate( 661 | (actual) => 662 | expected.difference(actual as DateTime).inMilliseconds.abs() < 100, 663 | 'is close to $expected'); 664 | --------------------------------------------------------------------------------