mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-21 23:11:45 +02:00
The AutoMix tests slept about 17 s of wall time waiting for fades, tempo recovery and "nothing happens" windows, and the haptics tests 1.15 s. fake_async is not a direct dependency, so the analyzer rejects importing it. Haptics tests run as testWidgets on the binding's FakeAsync clock, which also drives the PlaybackState position extrapolation. The AutoMix player shares process-wide runtime state (the listening recorder and personalization write chains), so a FakeAsync zone that stops after one test would strand later tests. test/support/virtual_time adds a zone with virtual timers and real microtasks instead: setUp builds the player in that zone, and fakeTimeTest runs a body with its timers fired in due order, skipping idle time. Pending timers continue on real time afterwards. AutoMix reads its fade and readiness clocks through autoMixStopwatch, which defaults to Stopwatch.new; tests point it, and the native fake's deck clocks, at the virtual clock. Test bodies and assertions are unchanged.
191 lines
4.8 KiB
Dart
191 lines
4.8 KiB
Dart
import 'dart:async';
|
|
|
|
/// Real time plus any idle time skipped by [VirtualTime.run].
|
|
///
|
|
/// Outside [run], timers created in [zone] are ordinary timers. Inside, they
|
|
/// are queued and fired in due order, skipping ahead whenever nothing else is
|
|
/// runnable, so a wait for a fade or for nothing to happen costs no real time.
|
|
/// Microtasks always stay real: no future or stream is tied to a zone that
|
|
/// stops running, even when shared runtime state outlives a test.
|
|
class VirtualTime {
|
|
VirtualTime() {
|
|
zone = _parent.fork(
|
|
specification: ZoneSpecification(
|
|
createTimer: (self, parent, zone, duration, callback) => _virtual
|
|
? _add(duration, null, (_) => callback())
|
|
: parent.createTimer(zone, duration, callback),
|
|
createPeriodicTimer: (self, parent, zone, period, callback) => _virtual
|
|
? _add(period, period, callback)
|
|
: parent.createPeriodicTimer(zone, period, callback),
|
|
),
|
|
);
|
|
}
|
|
|
|
final _parent = Zone.current;
|
|
late final Zone zone;
|
|
final _real = Stopwatch()..start();
|
|
final _timers = <_VirtualTimer>[];
|
|
var _skipped = Duration.zero;
|
|
var _virtual = false;
|
|
var _sequence = 0;
|
|
|
|
Duration get now => _real.elapsed + _skipped;
|
|
|
|
Stopwatch stopwatch() => _VirtualStopwatch(() => now);
|
|
|
|
Timer _add(
|
|
Duration duration,
|
|
Duration? period,
|
|
void Function(Timer) callback,
|
|
) {
|
|
final timer = _VirtualTimer(
|
|
this,
|
|
now + (duration.isNegative ? Duration.zero : duration),
|
|
period,
|
|
_sequence++,
|
|
callback,
|
|
);
|
|
_timers.add(timer);
|
|
return timer;
|
|
}
|
|
|
|
/// Runs [body] in [zone] with virtual timers until it completes.
|
|
Future<T> run<T>(Future<T> Function() body) async {
|
|
_virtual = true;
|
|
try {
|
|
final result = zone.run(body);
|
|
var done = false;
|
|
unawaited(
|
|
result
|
|
.then<void>((_) {}, onError: (Object _) {})
|
|
.whenComplete(() => done = true),
|
|
);
|
|
while (!done) {
|
|
// One real event-loop turn drains microtasks and platform replies.
|
|
await Future<void>.delayed(Duration.zero);
|
|
if (done || _timers.isEmpty) continue;
|
|
final next = _timers.reduce(
|
|
(a, b) => b.due < a.due || (b.due == a.due && b.sequence < a.sequence)
|
|
? b
|
|
: a,
|
|
);
|
|
if (next.due > now) _skipped += next.due - now;
|
|
if (_skipped > const Duration(minutes: 10)) {
|
|
throw StateError('Virtual time ran for more than ten minutes');
|
|
}
|
|
next._fire();
|
|
}
|
|
return await result;
|
|
} finally {
|
|
_virtual = false;
|
|
// Whatever is still scheduled continues on real time, as it would have.
|
|
for (final timer in _timers.toList()) {
|
|
timer._realize();
|
|
}
|
|
_timers.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
class _VirtualTimer implements Timer {
|
|
_VirtualTimer(this._time, this.due, this._period, this.sequence, this._run);
|
|
|
|
final VirtualTime _time;
|
|
final Duration? _period;
|
|
final int sequence;
|
|
final void Function(Timer) _run;
|
|
Duration due;
|
|
Timer? _real;
|
|
var _active = true;
|
|
var _tick = 0;
|
|
|
|
@override
|
|
bool get isActive => _active;
|
|
|
|
@override
|
|
int get tick => _tick;
|
|
|
|
@override
|
|
void cancel() {
|
|
_active = false;
|
|
_real?.cancel();
|
|
_time._timers.remove(this);
|
|
}
|
|
|
|
void _fire() {
|
|
final period = _period;
|
|
_tick++;
|
|
if (period == null) {
|
|
_active = false;
|
|
_time._timers.remove(this);
|
|
} else {
|
|
due += period;
|
|
}
|
|
_run(this);
|
|
}
|
|
|
|
void _realize() {
|
|
final remaining = due - _time.now;
|
|
_real = _time._parent.createTimer(
|
|
remaining.isNegative ? Duration.zero : remaining,
|
|
() {
|
|
final period = _period;
|
|
if (!_active) return;
|
|
_fire();
|
|
if (period != null && _active) {
|
|
_real = _time._parent.createPeriodicTimer(period, (_) {
|
|
if (!_active) return;
|
|
_tick++;
|
|
_run(this);
|
|
});
|
|
}
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A [Stopwatch] on [VirtualTime.now].
|
|
class _VirtualStopwatch implements Stopwatch {
|
|
_VirtualStopwatch(this._now);
|
|
|
|
final Duration Function() _now;
|
|
Duration? _startedAt;
|
|
Duration _stopped = Duration.zero;
|
|
|
|
@override
|
|
int get frequency => Duration.microsecondsPerSecond;
|
|
|
|
@override
|
|
bool get isRunning => _startedAt != null;
|
|
|
|
@override
|
|
void start() => _startedAt ??= _now();
|
|
|
|
@override
|
|
void stop() {
|
|
_stopped = elapsed;
|
|
_startedAt = null;
|
|
}
|
|
|
|
@override
|
|
void reset() {
|
|
_stopped = Duration.zero;
|
|
if (_startedAt != null) _startedAt = _now();
|
|
}
|
|
|
|
@override
|
|
Duration get elapsed {
|
|
final startedAt = _startedAt;
|
|
return startedAt == null ? _stopped : _stopped + _now() - startedAt;
|
|
}
|
|
|
|
@override
|
|
int get elapsedTicks => elapsed.inMicroseconds;
|
|
|
|
@override
|
|
int get elapsedMicroseconds => elapsed.inMicroseconds;
|
|
|
|
@override
|
|
int get elapsedMilliseconds => elapsed.inMilliseconds;
|
|
}
|