0380bbb1e7
- 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
211 lines
6.5 KiB
Dart
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);
|
|
});
|
|
});
|
|
}
|