118 lines
3.8 KiB
Dart
118 lines
3.8 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:pluriwave/servicios/controlador_reconexion.dart';
|
|
import 'package:pluriwave/servicios/servicio_audio.dart';
|
|
|
|
/// S6-R3 — Source-revision guard: rapid source switches must not let a stale
|
|
/// error from an earlier source bleed through to the final active source.
|
|
///
|
|
/// [PluriWaveAudioHandler] uses a monotonically-increasing [_revisionFuente]
|
|
/// counter; each [playMediaItem] call increments it and checks `revision ==
|
|
/// _revisionFuente` before every async step in [_cambiarFuente]. We cannot
|
|
/// instantiate the handler in unit tests (MethodChannels), so we exercise the
|
|
/// guard invariants through:
|
|
/// (a) the static buffer-configuration constant (already fully testable), and
|
|
/// (b) [ControladorReconexion.restablecer], which [playMediaItem] calls on
|
|
/// every fresh user switch to discard any in-flight backoff for the
|
|
/// previous source — the core "no stale error bleeds through" contract.
|
|
void main() {
|
|
group('Source-switch guard (S6-R3)', () {
|
|
test(
|
|
'rapid switch resets the reconnect controller so old backoff is discarded',
|
|
() {
|
|
final temporizadores = <_FakeTimer>[];
|
|
final controlador = ControladorReconexion(
|
|
crearTemporizador: (duracion, cb) {
|
|
final t = _FakeTimer(duracion, cb);
|
|
temporizadores.add(t);
|
|
return t;
|
|
},
|
|
);
|
|
|
|
// Simulate source A failing and scheduling a retry.
|
|
controlador.registrarFallo(
|
|
intencionReproducir: true,
|
|
alReintentar: () {},
|
|
);
|
|
expect(controlador.reintentoPendiente, isTrue);
|
|
|
|
// User rapidly switches to source B — playMediaItem calls restablecer().
|
|
controlador.restablecer();
|
|
|
|
expect(
|
|
controlador.reintentoPendiente,
|
|
isFalse,
|
|
reason: 'stale source-A retry must not fire after switching to B',
|
|
);
|
|
expect(
|
|
controlador.intentos,
|
|
0,
|
|
reason:
|
|
'backoff counter resets so source B starts with a clean slate',
|
|
);
|
|
expect(
|
|
temporizadores.single.cancelado,
|
|
isTrue,
|
|
reason: 'the pending timer for source A was cancelled',
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'three rapid switches all reset backoff independently (A→B→C keeps C clean)',
|
|
() {
|
|
final controlador = ControladorReconexion();
|
|
|
|
// A fails, schedule retry.
|
|
controlador.registrarFallo(
|
|
intencionReproducir: true,
|
|
alReintentar: () {},
|
|
);
|
|
// Switch to B — resets.
|
|
controlador.restablecer();
|
|
// B fails immediately too.
|
|
controlador.registrarFallo(
|
|
intencionReproducir: true,
|
|
alReintentar: () {},
|
|
);
|
|
// Switch to C — resets again.
|
|
controlador.restablecer();
|
|
|
|
expect(controlador.reintentoPendiente, isFalse);
|
|
expect(controlador.intentos, 0);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'buffer config constant carries the live-stream guard values (S7-R1)',
|
|
() {
|
|
// Ensures the static config used when recreating the player for a
|
|
// source switch carries the correct live-stream buffer parameters.
|
|
const config = PluriWaveAudioHandler.configuracionCargaAndroid;
|
|
final control = config.androidLoadControl;
|
|
expect(control, isNotNull);
|
|
expect(control!.minBufferDuration, const Duration(seconds: 15));
|
|
expect(control.maxBufferDuration, const Duration(seconds: 50));
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
class _FakeTimer implements Timer {
|
|
_FakeTimer(this.duracion, this.callback);
|
|
|
|
final Duration duracion;
|
|
final void Function() callback;
|
|
bool cancelado = false;
|
|
|
|
@override
|
|
void cancel() => cancelado = true;
|
|
|
|
@override
|
|
bool get isActive => !cancelado;
|
|
|
|
@override
|
|
int get tick => 0;
|
|
}
|