Files
pluriwave/test/servicios/servicio_audio_reconnect_test.dart
T
FreeTLab 0380bbb1e7 feat(streaming): buffer resilience and automatic reconnection
- Construct the audio player with an enlarged live-stream buffer (15-50s forward cushion, 2.5s to start, 5s after rebuffer) so short network drops play through silently
- Add reconnect-on-stall state machine with bounded exponential backoff (1/2/4/8/16s, ~90s total window, 5 attempts) that re-prepares to the live edge; backoff/decision logic extracted to controlador_reconexion.dart as pure testable code
- Surface a new reconnecting playback state in the mini player and full player (localized in all 13 locales) instead of error dialogs during the retry window; a single friendly error appears only after exhaustion
- Guard interplay: user pause/stop cancels retries, audio interruptions cancel reconnect, alarm wake-up path keeps precedence, recording fails cleanly during drops
- Reset retry budget on station change; route stream timeouts through the network-error class
- 10 new tests (99 total green), flutter analyze clean
2026-06-11 19:54:30 +02:00

211 lines
6.5 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';
/// Fake timer injected through the controller's timer factory so backoff
/// scheduling is observable without real delays or platform channels.
class _TemporizadorFalso implements Timer {
_TemporizadorFalso(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;
void disparar() {
if (!cancelado) callback();
}
}
ControladorReconexion _controlador({
int maxReintentos = 5,
Duration retrasoMaximo = const Duration(seconds: 30),
required List<_TemporizadorFalso> temporizadores,
}) {
return ControladorReconexion(
maxReintentos: maxReintentos,
retrasoMaximo: retrasoMaximo,
crearTemporizador: (duracion, callback) {
final timer = _TemporizadorFalso(duracion, callback);
temporizadores.add(timer);
return timer;
},
);
}
void main() {
group('ControladorReconexion — backoff (S7-R2-D, S7-R7)', () {
test('secuencia de retrasos 1-5 es [1s, 2s, 4s, 8s, 16s]', () {
final controlador = ControladorReconexion();
expect(
[for (var i = 1; i <= 5; i++) controlador.retrasoParaIntento(i)],
const [
Duration(seconds: 1),
Duration(seconds: 2),
Duration(seconds: 4),
Duration(seconds: 8),
Duration(seconds: 16),
],
);
});
test('el retraso queda limitado por retrasoMaximo', () {
final controlador = ControladorReconexion(
retrasoMaximo: Duration(seconds: 10),
);
expect(
controlador.retrasoParaIntento(5),
const Duration(seconds: 10),
reason: '16s sin limite, pero el maximo configurado es 10s',
);
expect(
ControladorReconexion().retrasoParaIntento(6),
const Duration(seconds: 30),
reason: '32s supera el maximo por defecto de 30s',
);
});
});
group('ControladorReconexion — decision de reintento (S7-R2, S7-R7)', () {
test('intencion=true + fallo => reintento programado con backoff', () {
final temporizadores = <_TemporizadorFalso>[];
final controlador = _controlador(temporizadores: temporizadores);
var reintentos = 0;
final decision = controlador.registrarFallo(
intencionReproducir: true,
alReintentar: () => reintentos++,
);
expect(decision, DecisionReconexion.reintentar);
expect(controlador.intentos, 1);
expect(controlador.reintentoPendiente, isTrue);
expect(temporizadores, hasLength(1));
expect(temporizadores.single.duracion, const Duration(seconds: 1));
temporizadores.single.disparar();
expect(reintentos, 1, reason: 'el callback corre al vencer el backoff');
});
test('intencion=false + fallo => NO se programa reintento (S7-R2-B)', () {
final temporizadores = <_TemporizadorFalso>[];
final controlador = _controlador(temporizadores: temporizadores);
final decision = controlador.registrarFallo(
intencionReproducir: false,
alReintentar: () => fail('no debe reintentar con intencion=false'),
);
expect(decision, DecisionReconexion.ignorar);
expect(controlador.intentos, 0);
expect(controlador.reintentoPendiente, isFalse);
expect(temporizadores, isEmpty);
});
test('tras maxReintentos agotados => agotado, sin mas reintentos '
'(S7-R2-C)', () {
final temporizadores = <_TemporizadorFalso>[];
final controlador = _controlador(
maxReintentos: 2,
temporizadores: temporizadores,
);
for (var i = 0; i < 2; i++) {
expect(
controlador.registrarFallo(
intencionReproducir: true,
alReintentar: () {},
),
DecisionReconexion.reintentar,
);
}
final decision = controlador.registrarFallo(
intencionReproducir: true,
alReintentar: () => fail('no debe programar tras agotar reintentos'),
);
expect(decision, DecisionReconexion.agotado);
expect(controlador.reintentoPendiente, isFalse);
expect(temporizadores, hasLength(2));
});
test('una reconexion exitosa restablece el contador (S7-R7)', () {
final temporizadores = <_TemporizadorFalso>[];
final controlador = _controlador(temporizadores: temporizadores);
controlador.registrarFallo(
intencionReproducir: true,
alReintentar: () {},
);
controlador.registrarFallo(
intencionReproducir: true,
alReintentar: () {},
);
expect(controlador.intentos, 2);
controlador.restablecer();
expect(controlador.intentos, 0);
expect(controlador.reintentoPendiente, isFalse);
controlador.registrarFallo(
intencionReproducir: true,
alReintentar: () {},
);
expect(
temporizadores.last.duracion,
const Duration(seconds: 1),
reason: 'tras restablecer, el backoff arranca de nuevo en la base',
);
});
test('stop del usuario durante el stall cancela el reintento (S7-R6)', () {
final temporizadores = <_TemporizadorFalso>[];
final controlador = _controlador(temporizadores: temporizadores);
var reintentos = 0;
controlador.registrarFallo(
intencionReproducir: true,
alReintentar: () => reintentos++,
);
expect(controlador.reintentoPendiente, isTrue);
controlador.cancelar();
expect(controlador.reintentoPendiente, isFalse);
expect(temporizadores.single.cancelado, isTrue);
temporizadores.single.disparar();
expect(reintentos, 0, reason: 'un timer cancelado nunca reintenta');
});
});
group('Buffer de stream en vivo (S7-R1)', () {
test('la configuracion Android usa los valores del diseno 7.1', () {
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));
expect(
control.bufferForPlaybackDuration,
const Duration(milliseconds: 2500),
);
expect(
control.bufferForPlaybackAfterRebufferDuration,
const Duration(seconds: 5),
);
expect(control.prioritizeTimeOverSizeThresholds, isTrue);
});
});
}