| // Copyright (c) 2026, 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. |
| |
| // Test that an `async*` stream continues running in the same zone after a yield. |
| // Tries to pause, resume or cancel it from another zone. |
| |
| import "dart:async"; |
| |
| import "package:expect/async_helper.dart"; |
| import "package:expect/expect.dart"; |
| |
| String get zoneId => Zone.current[#id] ?? '<unknown>'; |
| |
| void main() async { |
| asyncStart(); |
| String testName = ""; |
| |
| Stream<int> makeStream(int n) async* { |
| Zone zone = Zone.current; |
| Expect.equals('outer', zoneId, 'expected start zone: $zoneId'); |
| try { |
| for (var i = 0; i < n; i++) { |
| yield i; |
| Expect.equals( |
| zone, |
| Zone.current, |
| '$testName: current zone after yield: $zoneId', |
| ); |
| } |
| } finally { |
| Expect.equals( |
| zone, |
| Zone.current, |
| '$testName: current zone at end or after cancel: $zoneId', |
| ); |
| } |
| } |
| |
| Stream<int> repeatStream(int n, {int recurse = 1}) async* { |
| Zone zone = Zone.current; |
| try { |
| yield* (recurse > 1 |
| ? repeatStream(n, recurse: recurse - 1) |
| : makeStream(n)); |
| } finally { |
| Expect.equals( |
| zone, |
| Zone.current, |
| '$testName: current zone after yield*: $zoneId', |
| ); |
| } |
| } |
| |
| // Sibling zone of zone where tests are run. |
| final parallelZone = Zone.current.fork(zoneValues: {#id: 'parallel'}); |
| // Run tests in zone with `zoneId == 'outer'`. |
| await runZoned<Future<void>>(zoneValues: {#id: 'outer'}, () async { |
| // Nested zone with different zone-ID. |
| Zone nestedZone = Zone.current.fork(zoneValues: {#id: "nested"}); |
| |
| Future<void> testPause( |
| String name, |
| Stream<int> stream, |
| int count, |
| Zone otherZone, |
| ) { |
| testName = "$name-pause"; |
| print(testName); |
| Expect.equals('outer', zoneId, testName); // Sanity check. |
| var done = Completer<void>(); |
| var expected = 0; |
| var subscription = stream.listen( |
| null, |
| onError: done.completeError, |
| onDone: () { |
| if (!done.isCompleted) done.complete(); |
| }, |
| ); |
| subscription.onData((value) { |
| Expect.equals(expected, value, name); |
| expected++; |
| if (expected > count) Expect.fail("$name: too many events"); |
| otherZone.run(() { |
| subscription.pause(); // Pause in different zone. |
| }); |
| Timer(Duration.zero, () { |
| subscription.resume(); // Resume later (in original zone) |
| }); |
| }); |
| return done.future; |
| } |
| |
| await testPause('nested-make', makeStream(5), 5, nestedZone); |
| await testPause('nested-repeat', repeatStream(5), 5, nestedZone); |
| await testPause( |
| 'nested-repeat-2', |
| repeatStream(5, recurse: 2), |
| 5, |
| nestedZone, |
| ); |
| |
| await testPause('parallel-make', makeStream(5), 5, parallelZone); |
| await testPause('parallel-repeat', repeatStream(5), 5, parallelZone); |
| await testPause( |
| 'parallel-repeat-2', |
| repeatStream(5, recurse: 2), |
| 5, |
| parallelZone, |
| ); |
| |
| Future<void> testResume( |
| String name, |
| Stream<int> stream, |
| int count, |
| Zone otherZone, |
| ) { |
| testName = "$name-resume"; |
| print(testName); |
| Expect.equals('outer', zoneId, testName); // Sanity check. |
| var done = Completer<void>(); |
| var expected = 0; |
| var subscription = stream.listen( |
| null, |
| onError: done.completeError, |
| onDone: () { |
| if (!done.isCompleted) done.complete(); |
| }, |
| ); |
| subscription.onData((value) { |
| Expect.equals(expected, value, name); |
| expected++; |
| if (expected > count) Expect.fail("$name: too many events"); |
| subscription.pause(); |
| otherZone.scheduleMicrotask(() { |
| subscription.resume(); |
| }); |
| }); |
| return done.future; |
| } |
| |
| await testResume('nested-make', makeStream(5), 5, nestedZone); |
| await testResume('nested-repeat', repeatStream(5), 5, nestedZone); |
| await testResume( |
| 'nested-repeat-2', |
| repeatStream(5, recurse: 2), |
| 5, |
| nestedZone, |
| ); |
| |
| await testResume('parallel-make', makeStream(5), 5, parallelZone); |
| await testResume('parallel-repeat', repeatStream(5), 5, parallelZone); |
| await testResume( |
| 'parallel-repeat-2', |
| repeatStream(5, recurse: 2), |
| 5, |
| parallelZone, |
| ); |
| |
| Future<void> testResumeFuture( |
| String name, |
| Stream<int> stream, |
| int count, |
| Zone otherZone, |
| ) { |
| testName = '$name-resume-future'; |
| print(testName); |
| Expect.equals('outer', zoneId, testName); // Sanity check. |
| var done = Completer<void>(); |
| var expected = 0; |
| var subscription = stream.listen( |
| null, |
| onError: done.completeError, |
| onDone: () { |
| if (!done.isCompleted) done.complete(); |
| }, |
| ); |
| subscription.onData((value) { |
| Expect.equals(expected, value, name); |
| expected++; |
| if (expected > count) Expect.fail("$name: too many events"); |
| otherZone.run(() { |
| subscription.pause(Future<void>.microtask(() {})); |
| }); |
| }); |
| return done.future; |
| } |
| |
| await testResumeFuture('nested-make', makeStream(5), 5, nestedZone); |
| await testResumeFuture('nested-repeat', repeatStream(5), 5, nestedZone); |
| await testResumeFuture( |
| 'nested-repeat-2', |
| repeatStream(5, recurse: 2), |
| 5, |
| nestedZone, |
| ); |
| |
| await testResumeFuture('parallel-make', makeStream(5), 5, parallelZone); |
| await testResumeFuture('parallel-repeat', repeatStream(5), 5, parallelZone); |
| await testResumeFuture( |
| 'parallel-repeat-2', |
| repeatStream(5, recurse: 2), |
| 5, |
| parallelZone, |
| ); |
| |
| Future<void> testCancel( |
| String name, |
| Stream<int> stream, |
| int count, |
| Zone otherZone, |
| ) { |
| testName = '$name-cancel'; |
| print(testName); |
| Expect.equals('outer', zoneId, testName); // Sanity check. |
| var done = Completer<void>(); |
| var expected = 0; |
| var subscription = stream.listen( |
| null, |
| onError: done.completeError, |
| onDone: () { |
| if (!done.isCompleted) done.complete(); |
| }, |
| ); |
| subscription.onData((value) { |
| Expect.equals(expected, value, name); |
| expected++; |
| if (expected == count) { |
| otherZone.run(() { |
| done.complete(subscription.cancel()); |
| }); |
| } |
| }); |
| return done.future; |
| } |
| |
| await testCancel('nested-make', makeStream(5), 5, nestedZone); |
| await testCancel('nested-repeat', repeatStream(5), 5, nestedZone); |
| await testCancel( |
| 'nested-repeat-2', |
| repeatStream(5, recurse: 2), |
| 5, |
| nestedZone, |
| ); |
| |
| await testCancel('parallel-make', makeStream(5), 5, parallelZone); |
| await testCancel('parallel-repeat', repeatStream(5), 5, parallelZone); |
| await testCancel( |
| 'parallel-repeat-2', |
| repeatStream(5, recurse: 2), |
| 5, |
| parallelZone, |
| ); |
| |
| asyncEnd(); |
| }); |
| } |