fake_async icon indicating copy to clipboard operation
fake_async copied to clipboard

FakeAsync hangs when operating on a Stream

Open eximius313 opened this issue 2 years ago • 2 comments

This test:

        test('Should interrupt on timeout', () {
            //given
            var controller = StreamController<Uint8List>();
            const expectedError = 'No response';

            //when
            result() {
              final comleter = Completer<bool>();
              controller.stream.timeout(
                Duration(seconds: 5),
                onTimeout: (sink) {
                  print('on timeout!');
                  sink.close();
                },
              ).listen((event) {
              }, onDone: () {
                print('onDone');
                comleter.completeError(Exception(expectedError));
              });
              return comleter.future;
            }

            //then
            expect(result, throwsA(predicate((e) => e is Exception && e.toString() == 'Exception: $expectedError')));
        });

passes as expected after 5 seconds. But when I apply FakeAsync:

        test('Should interrupt on timeout', () {
          fakeAsync((async) {
            //given
            var controller = StreamController<Uint8List>();
            const expectedError = 'No response';

            //when
            result() {
              final comleter = Completer<bool>();
              controller.stream.timeout(
                Duration(seconds: 5),
                onTimeout: (sink) {
                  print('on timeout!');
                  sink.close();
                },
              ).listen((event) {
              }, onDone: () {
                print('onDone');
                comleter.completeError(Exception(expectedError));
              });
              return comleter.future;
            }

            //then
            expect(result, throwsA(predicate((e) => e is Exception && e.toString() == 'Exception: $expectedError')));

            async.elapse(Duration(seconds: 6));
          });
        });

it displays:

on timeout!
onDone

as expeceted, but then test hangs and is interrupted after 30sec by timeout:

00:31 +0 -1: Should interrupt on timeout [E]
  TimeoutException after 0:00:30.000000: Test timed out after 30 seconds. See https://pub.dev/packages/test#timeouts
  dart:isolate  _RawReceivePort._handleMessage

eximius313 avatar Aug 11 '23 15:08 eximius313

Try moving the final comleter = Completer<bool>(); outside of the fakeAsync zone.

It's hard to reason about fakeAsync, but my experience is that you should never rely on anything created inside the fake-async zone to happen, or not happen, unless you are actively elapsing time. Don't trust that anything can cross the zone boundary by itself, because that requires something driving the internal progress.

So, if possible, I try to make sure elapsing is done by code running outside of the fake-async zone (because otherwise we can get a deadlock when nothing elapses to the microtask which should run the async.elapse(...), and try to push results outside of the zone as well, to check them there. (But I don't have a pattern that I know will always work.)

lrhn avatar Feb 03 '24 14:02 lrhn

thanks @lrhn, this code:

void main() {
  test('Should interrupt on timeout', () {
    final completer = Completer<bool>();
    fakeAsync((async) {
      //given
      var controller = StreamController<Uint8List>();
      const expectedError = 'No response';

      //when
      result() {
        controller.stream.timeout(
          Duration(seconds: 5),
          onTimeout: (sink) {
            print('on timeout!');
            sink.close();
          },
        ).listen((event) {
        }, onDone: () {
          print('onDone');
          completer.completeError(Exception(expectedError));
        });
        return completer.future;
      }

      //then
      expect(result, throwsA(predicate((e) => e is Exception && e.toString() == 'Exception: $expectedError')));

      async.elapse(Duration(seconds: 6));
    });
  });
}

indeed works - although I have no idea why, because for me:

  1. Code inside fakeAsync((async) { was executed, because 'onDone' was printed on the console
  2. it is the completer.completeError(Exception(expectedError)); who should end the future so what's creation of final completer = Completer<bool>(); has to do with it?

Kind regards

eximius313 avatar Feb 06 '24 18:02 eximius313