blob: 1d03e235dae785cb26eff9b50b2b9e74a708ba3b [file] [edit]
// 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();
});
}