// Copyright 2015 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import 'dart:async' show Completer, FutureOr; import 'package:matcher/expect.dart' as m; import 'package:stack_trace/stack_trace.dart' show Chain; const defaultInterval = Duration(milliseconds: 500); const defaultTimeout = Duration(seconds: 5); const clock = Clock(); Future waitFor(FutureOr Function() condition, {Object? matcher, Duration timeout = defaultTimeout, Duration interval = defaultInterval, String? reason}) => clock.waitFor(condition, matcher: matcher, timeout: timeout, interval: interval, reason: reason); class Clock { const Clock(); /// Sleep for the specified time. Future sleep([Duration interval = defaultInterval]) => Future.delayed(interval); /// The current time. DateTime get now => DateTime.now(); /// Waits until [condition] evaluates to a value that matches [matcher] or /// until [timeout] time has passed. If [condition] returns a [Future], then /// uses the value of that [Future] rather than the value of [condition]. /// /// If the wait is successful, then the matching return value of [condition] /// is returned. Otherwise, if [condition] throws, then that exception is /// rethrown. If [condition] doesn't throw then an [expect] exception is /// thrown. /// /// [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. Future waitFor(FutureOr Function() condition, {Object? matcher, Duration timeout = defaultTimeout, Duration interval = defaultInterval, String? reason}) async { final mMatcher = matcher == null ? null : m.wrapMatcher(matcher); final endTime = now.add(timeout); while (true) { try { final value = await condition(); if (mMatcher != null) { _matcherExpect(value, mMatcher, reason); } return value; } catch (e) { if (now.isAfter(endTime)) { rethrow; } else { await sleep(interval); } } } } } void _matcherExpect(Object? value, m.Matcher matcher, String? reason) { final matchState = {}; if (matcher.matches(value, matchState)) { return; } final desc = m.StringDescription() ..add('Expected: ') ..addDescriptionOf(matcher) ..add('\n') ..add(' Actual: ') ..addDescriptionOf(value) ..add('\n'); final mismatchDescription = m.StringDescription(); matcher.describeMismatch(value, mismatchDescription, matchState, true); if (mismatchDescription.length > 0) { desc.add(' Which: $mismatchDescription\n'); } if (reason != null) { desc.add('$reason\n'); } m.fail(desc.toString()); } class Lock { Completer? _lock; Chain? _stack; final bool awaitChecking; Lock({this.awaitChecking = false}); Future acquire() { if (awaitChecking) { if (isHeld) { return Future.error(StateError( 'Maybe you missed an await? Lock is already held by:\n$_stack')); } else { _stack = Chain.current().terse; _lock = Completer(); return Future.value(); } } else { return () async { while (isHeld) { await _lock!.future; } _lock = Completer(); }(); } } void release() { if (!isHeld) { throw StateError('No lock to release'); } _lock!.complete(); _lock = null; _stack = null; } bool get isHeld => _lock != null; }