Convert the 4 EQ persistence readers (device presets, matrix presets, device names, per-station presets) to per-entry tolerant parsing via the shared persistencia_tolerante helper, so one corrupt entry no longer discards every sibling preset. The principal-preset reader gains diagnostic logging on its existing fallback path. No degraded flag or quarantine here (unlike alarms/stations) since EQ writes are explicit-only and presets are trivially re-creatable.
574 lines
20 KiB
Dart
574 lines
20 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
|
import 'package:pluriwave/servicios/servicio_ecualizador.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
Map<String, dynamic> _presetJson(String nombre) => {
|
|
'nombre': nombre,
|
|
'bandas': [1.0, 2.0, 3.0, 4.0, 5.0],
|
|
};
|
|
|
|
void main() {
|
|
setUp(() {
|
|
SharedPreferences.setMockInitialValues({});
|
|
});
|
|
|
|
group('ServicioEcualizador — multi-device persistence (Phase 4)', () {
|
|
test('4.1a toggle defaults to false when no SP data', () async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
final config = await servicio.cargar();
|
|
expect(config.eqMultiDeviceEnabled, isFalse);
|
|
});
|
|
|
|
test(
|
|
'4.1b guardarToggleMultiDispositivo persists and cargar restores it',
|
|
() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
|
|
await servicio.guardarToggleMultiDispositivo(true);
|
|
final config = await servicio.cargar();
|
|
|
|
expect(config.eqMultiDeviceEnabled, isTrue);
|
|
},
|
|
);
|
|
|
|
test('4.1c guardarPresetDispositivo stores a device preset', () async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
const deviceId = 'bt_a2dp:AA:BB:CC:DD:EE:FF';
|
|
|
|
await servicio.guardarPresetDispositivo(deviceId, PresetEcualizador.jazz);
|
|
final config = await servicio.cargar();
|
|
|
|
expect(
|
|
config.presetsDispositivo[deviceId],
|
|
equals(PresetEcualizador.jazz),
|
|
);
|
|
});
|
|
|
|
test(
|
|
'4.1d obtenerPresetDispositivo returns null for unknown device',
|
|
() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
final config = await servicio.cargar();
|
|
|
|
expect(config.presetsDispositivo['unknown_device'], isNull);
|
|
},
|
|
);
|
|
|
|
test('4.1e guardarPresetMatriz stores a matrix entry', () async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
const clave = 'station-uuid-123:bt_a2dp:AA:BB:CC:DD:EE:FF';
|
|
|
|
await servicio.guardarPresetMatriz(clave, PresetEcualizador.rock);
|
|
final config = await servicio.cargar();
|
|
|
|
expect(config.presetsMatriz[clave], equals(PresetEcualizador.rock));
|
|
});
|
|
|
|
test('4.1f multiple device presets persist independently', () async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
|
|
await servicio.guardarPresetDispositivo(
|
|
'bt_a2dp:11:22:33:44:55:66',
|
|
PresetEcualizador.rock,
|
|
);
|
|
await servicio.guardarPresetDispositivo(
|
|
'wired_headset',
|
|
PresetEcualizador.jazz,
|
|
);
|
|
|
|
final config = await servicio.cargar();
|
|
expect(
|
|
config.presetsDispositivo['bt_a2dp:11:22:33:44:55:66'],
|
|
equals(PresetEcualizador.rock),
|
|
);
|
|
expect(
|
|
config.presetsDispositivo['wired_headset'],
|
|
equals(PresetEcualizador.jazz),
|
|
);
|
|
});
|
|
|
|
test('4.1g multiple matrix entries persist independently', () async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
|
|
await servicio.guardarPresetMatriz(
|
|
'station-A:bt_a2dp:AA:BB:CC:DD:EE:FF',
|
|
PresetEcualizador.jazz,
|
|
);
|
|
await servicio.guardarPresetMatriz(
|
|
'station-B:wired_headset',
|
|
PresetEcualizador.bassBoost,
|
|
);
|
|
|
|
final config = await servicio.cargar();
|
|
expect(
|
|
config.presetsMatriz['station-A:bt_a2dp:AA:BB:CC:DD:EE:FF'],
|
|
equals(PresetEcualizador.jazz),
|
|
);
|
|
expect(
|
|
config.presetsMatriz['station-B:wired_headset'],
|
|
equals(PresetEcualizador.bassBoost),
|
|
);
|
|
});
|
|
|
|
test(
|
|
'4.1h existing principal/emisora fields unaffected by new fields',
|
|
() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
await servicio.guardarPrincipal(PresetEcualizador.pop);
|
|
await servicio.guardarPorEmisora('station-X', PresetEcualizador.voz);
|
|
await servicio.guardarToggleMultiDispositivo(true);
|
|
await servicio.guardarPresetDispositivo(
|
|
'builtin_speaker',
|
|
PresetEcualizador.jazz,
|
|
);
|
|
|
|
final config = await servicio.cargar();
|
|
expect(config.principal, equals(PresetEcualizador.pop));
|
|
expect(config.porEmisora['station-X'], equals(PresetEcualizador.voz));
|
|
expect(config.eqMultiDeviceEnabled, isTrue);
|
|
expect(
|
|
config.presetsDispositivo['builtin_speaker'],
|
|
equals(PresetEcualizador.jazz),
|
|
);
|
|
},
|
|
);
|
|
|
|
test('4.1i guardarConfiguracion with new fields round-trips', () async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
final config = ConfiguracionEcualizador(
|
|
principal: PresetEcualizador.rock,
|
|
porEmisora: {'station-1': PresetEcualizador.jazz},
|
|
activo: true,
|
|
eqMultiDeviceEnabled: true,
|
|
presetsDispositivo: {
|
|
'bt_a2dp:AA:BB:CC:DD:EE:FF': PresetEcualizador.voz,
|
|
},
|
|
presetsMatriz: {
|
|
'station-1:bt_a2dp:AA:BB:CC:DD:EE:FF': PresetEcualizador.bassBoost,
|
|
},
|
|
);
|
|
|
|
await servicio.guardarConfiguracion(config);
|
|
final restored = await servicio.cargar();
|
|
|
|
expect(restored.principal, equals(PresetEcualizador.rock));
|
|
expect(restored.porEmisora['station-1'], equals(PresetEcualizador.jazz));
|
|
expect(restored.eqMultiDeviceEnabled, isTrue);
|
|
expect(
|
|
restored.presetsDispositivo['bt_a2dp:AA:BB:CC:DD:EE:FF'],
|
|
equals(PresetEcualizador.voz),
|
|
);
|
|
expect(
|
|
restored.presetsMatriz['station-1:bt_a2dp:AA:BB:CC:DD:EE:FF'],
|
|
equals(PresetEcualizador.bassBoost),
|
|
);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Phase 1 (eq-device-autoswitch-ux): nombresDispositivos persistence
|
|
// ---------------------------------------------------------------------------
|
|
|
|
group('ServicioEcualizador — nombresDispositivos (eq-device-autoswitch-ux)', () {
|
|
// 1.1 RED — cargarNombresDispositivos returns empty map when SP key absent
|
|
test('1.1 cargar returns empty nombresDispositivos when SP key absent', () async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
final config = await servicio.cargar();
|
|
expect(config.nombresDispositivos, isEmpty);
|
|
});
|
|
|
|
// 1.2 RED — guardarNombresDispositivos writes to eq_nombres_dispositivos_v1 SP key
|
|
test('1.2 guardarNombresDispositivos persists names and cargar restores them', () async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
|
|
await servicio.guardarNombresDispositivos({'bt_a2dp:AA:BB': 'Living Room BT'});
|
|
final config = await servicio.cargar();
|
|
|
|
expect(config.nombresDispositivos['bt_a2dp:AA:BB'], equals('Living Room BT'));
|
|
});
|
|
|
|
// 1.3 RED — round-trip: save → load returns same map
|
|
test('1.3 round-trip save → cargar preserves all entries', () async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
const nombres = {
|
|
'bt_a2dp:AA:BB': 'Living Room BT',
|
|
'wired_headset': 'Office Headset',
|
|
'builtin_speaker': 'Built-in',
|
|
};
|
|
|
|
await servicio.guardarNombresDispositivos(nombres);
|
|
final config = await servicio.cargar();
|
|
|
|
expect(config.nombresDispositivos, equals(nombres));
|
|
});
|
|
|
|
// guardarConfiguracion also persists nombresDispositivos
|
|
test('1.7 guardarConfiguracion persists nombresDispositivos round-trip', () async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
final config = ConfiguracionEcualizador(
|
|
principal: PresetEcualizador.flat,
|
|
porEmisora: {},
|
|
eqMultiDeviceEnabled: true,
|
|
presetsDispositivo: {},
|
|
presetsMatriz: {},
|
|
nombresDispositivos: {'bt_a2dp:AA:BB': 'My Speaker'},
|
|
);
|
|
|
|
await servicio.guardarConfiguracion(config);
|
|
final restored = await servicio.cargar();
|
|
|
|
expect(restored.nombresDispositivos['bt_a2dp:AA:BB'], equals('My Speaker'));
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// bt-device-identity Phase 5: one-time guarded migration purge
|
|
// ---------------------------------------------------------------------------
|
|
|
|
group('ServicioEcualizador — placeholder migration purge (bt-device-identity Phase 5)', () {
|
|
const placeholderKey = 'bt_a2dp:02:00:00:00:00:00';
|
|
const stableKey = 'bt_a2dp:AA:BB:CC:DD:EE:FF';
|
|
|
|
test(
|
|
'5.1 migration removes only exact placeholder entries from presetsPorDispositivo',
|
|
() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
await servicio.guardarPresetDispositivo(
|
|
placeholderKey,
|
|
PresetEcualizador.jazz,
|
|
);
|
|
await servicio.guardarPresetDispositivo(
|
|
stableKey,
|
|
PresetEcualizador.rock,
|
|
);
|
|
|
|
await servicio.migrarClavesPlaceholder();
|
|
final config = await servicio.cargar();
|
|
|
|
expect(
|
|
config.presetsDispositivo.containsKey(placeholderKey),
|
|
isFalse,
|
|
);
|
|
expect(
|
|
config.presetsDispositivo[stableKey],
|
|
equals(PresetEcualizador.rock),
|
|
);
|
|
},
|
|
);
|
|
|
|
test('5.2 matrix keys purge only the placeholder segment', () async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
await servicio.guardarPresetMatriz(
|
|
'station1:$placeholderKey',
|
|
PresetEcualizador.jazz,
|
|
);
|
|
await servicio.guardarPresetMatriz(
|
|
'station1:$stableKey',
|
|
PresetEcualizador.rock,
|
|
);
|
|
|
|
await servicio.migrarClavesPlaceholder();
|
|
final config = await servicio.cargar();
|
|
|
|
expect(
|
|
config.presetsMatriz.containsKey('station1:$placeholderKey'),
|
|
isFalse,
|
|
);
|
|
expect(
|
|
config.presetsMatriz['station1:$stableKey'],
|
|
equals(PresetEcualizador.rock),
|
|
);
|
|
});
|
|
|
|
test('5.3 near-miss keys are preserved (differs by one digit)', () async {
|
|
const nearMissKey = 'bt_a2dp:02:00:00:00:00:01';
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
await servicio.guardarPresetDispositivo(
|
|
nearMissKey,
|
|
PresetEcualizador.jazz,
|
|
);
|
|
|
|
await servicio.migrarClavesPlaceholder();
|
|
final config = await servicio.cargar();
|
|
|
|
expect(
|
|
config.presetsDispositivo[nearMissKey],
|
|
equals(PresetEcualizador.jazz),
|
|
);
|
|
});
|
|
|
|
test('5.4 migration runs once (second run is a true no-op)', () async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
await servicio.guardarPresetDispositivo(
|
|
placeholderKey,
|
|
PresetEcualizador.jazz,
|
|
);
|
|
await servicio.guardarPresetDispositivo(
|
|
stableKey,
|
|
PresetEcualizador.rock,
|
|
);
|
|
|
|
await servicio.migrarClavesPlaceholder();
|
|
|
|
// Re-seed the placeholder key directly, bypassing the flag, to prove
|
|
// the SECOND migration call is a true no-op (flag short-circuits)
|
|
// rather than merely finding nothing left to purge.
|
|
await servicio.guardarPresetDispositivo(
|
|
placeholderKey,
|
|
PresetEcualizador.bassBoost,
|
|
);
|
|
await servicio.migrarClavesPlaceholder();
|
|
|
|
final config = await servicio.cargar();
|
|
|
|
expect(
|
|
config.presetsDispositivo[placeholderKey],
|
|
equals(PresetEcualizador.bassBoost),
|
|
);
|
|
expect(
|
|
config.presetsDispositivo[stableKey],
|
|
equals(PresetEcualizador.rock),
|
|
);
|
|
});
|
|
|
|
test(
|
|
'5.5 no placeholder entries when BLUETOOTH_CONNECT was never '
|
|
'requested is a no-op',
|
|
() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
await servicio.guardarPresetDispositivo(
|
|
stableKey,
|
|
PresetEcualizador.rock,
|
|
);
|
|
|
|
await servicio.migrarClavesPlaceholder();
|
|
final config = await servicio.cargar();
|
|
|
|
expect(
|
|
config.presetsDispositivo[stableKey],
|
|
equals(PresetEcualizador.rock),
|
|
);
|
|
expect(config.presetsDispositivo.length, equals(1));
|
|
},
|
|
);
|
|
|
|
test('5.6 nombresDispositivos also purges the exact placeholder key', () async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
await servicio.guardarNombresDispositivos({
|
|
placeholderKey: 'Ghost Device',
|
|
stableKey: 'Living Room BT',
|
|
});
|
|
|
|
await servicio.migrarClavesPlaceholder();
|
|
final config = await servicio.cargar();
|
|
|
|
expect(config.nombresDispositivos.containsKey(placeholderKey), isFalse);
|
|
expect(
|
|
config.nombresDispositivos[stableKey],
|
|
equals('Living Room BT'),
|
|
);
|
|
});
|
|
|
|
test('5.9 cargar() runs the migration automatically on first load', () async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
await servicio.guardarPresetDispositivo(
|
|
placeholderKey,
|
|
PresetEcualizador.jazz,
|
|
);
|
|
await servicio.guardarPresetDispositivo(
|
|
stableKey,
|
|
PresetEcualizador.rock,
|
|
);
|
|
|
|
// No explicit migrarClavesPlaceholder() call: cargar() must run it.
|
|
final config = await servicio.cargar();
|
|
|
|
expect(config.presetsDispositivo.containsKey(placeholderKey), isFalse);
|
|
expect(
|
|
config.presetsDispositivo[stableKey],
|
|
equals(PresetEcualizador.rock),
|
|
);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// persistence-resilience Phase C: per-entry tolerant reads for the 4 EQ
|
|
// map/single-value readers + degraded-read logging. NO flag/quarantine
|
|
// here (D6, intentional asymmetry vs. Alarms/Stations) — EQ presets are
|
|
// explicit-only writes and trivially re-creatable.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
group(
|
|
'ServicioEcualizador — lectura tolerante y diagnostico (persistence-resilience)',
|
|
() {
|
|
late DebugPrintCallback debugPrintOriginal;
|
|
late List<String> logs;
|
|
|
|
setUp(() {
|
|
debugPrintOriginal = debugPrint;
|
|
logs = [];
|
|
debugPrint = (String? message, {int? wrapWidth}) {
|
|
if (message != null) logs.add(message);
|
|
};
|
|
});
|
|
|
|
tearDown(() {
|
|
debugPrint = debugPrintOriginal;
|
|
});
|
|
|
|
test(
|
|
'C2 presetsMatriz: sobrevive parcial con 2 entradas validas + 1 valor '
|
|
'corrupto, claves identicas al original',
|
|
() async {
|
|
final raw = jsonEncode({
|
|
'station-1:device-a': _presetJson('Rock'),
|
|
'station-2:device-b': _presetJson('Pop'),
|
|
'station-3:device-c': {'nombre': 'Malo', 'bandas': 'no-es-lista'},
|
|
});
|
|
SharedPreferences.setMockInitialValues({
|
|
'eq_presets_matriz_v1': raw,
|
|
});
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
|
|
final config = await servicio.cargar();
|
|
|
|
expect(config.presetsMatriz.keys.toSet(), {
|
|
'station-1:device-a',
|
|
'station-2:device-b',
|
|
});
|
|
expect(config.presetsMatriz['station-1:device-a']!.nombre, 'Rock');
|
|
expect(config.presetsMatriz['station-2:device-b']!.nombre, 'Pop');
|
|
},
|
|
);
|
|
|
|
test(
|
|
'C3 presetsMatriz: clave sobreviviente con deviceId estilo MAC '
|
|
'(varios dos puntos) queda intacta pese a un vecino corrupto',
|
|
() async {
|
|
const claveMac = 'station-X:bt_a2dp:AA:BB:CC:DD:EE:FF';
|
|
final raw = jsonEncode({
|
|
claveMac: _presetJson('Jazz'),
|
|
'station-Y:wired_headset': _presetJson('Rock'),
|
|
'station-Z:corrupt': {'nombre': 'Malo', 'bandas': 'no-es-lista'},
|
|
});
|
|
SharedPreferences.setMockInitialValues({
|
|
'eq_presets_matriz_v1': raw,
|
|
});
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
|
|
final config = await servicio.cargar();
|
|
|
|
expect(config.presetsMatriz.keys.toSet(), {
|
|
claveMac,
|
|
'station-Y:wired_headset',
|
|
});
|
|
expect(config.presetsMatriz[claveMac]!.nombre, 'Jazz');
|
|
},
|
|
);
|
|
|
|
test(
|
|
'C4 presetsDispositivo totalmente corrupto: cargar() queda vacio, '
|
|
'guardarPresetDispositivo sigue escribiendo (sin bandera en EQ)',
|
|
() async {
|
|
SharedPreferences.setMockInitialValues({
|
|
'eq_preset_por_dispositivo_v1': '{bad',
|
|
});
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
|
|
final config = await servicio.cargar();
|
|
expect(config.presetsDispositivo, isEmpty);
|
|
|
|
const deviceId = 'bt_a2dp:AA:BB:CC:DD:EE:FF';
|
|
await servicio.guardarPresetDispositivo(
|
|
deviceId,
|
|
PresetEcualizador.rock,
|
|
);
|
|
final tras = await servicio.cargar();
|
|
|
|
expect(
|
|
tras.presetsDispositivo[deviceId],
|
|
equals(PresetEcualizador.rock),
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'C5 preset principal totalmente corrupto: cargar() retorna flat y '
|
|
'loguea la lectura degradada',
|
|
() async {
|
|
SharedPreferences.setMockInitialValues({
|
|
'eq_preset_principal_v1': '{bad',
|
|
});
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
|
|
final config = await servicio.cargar();
|
|
|
|
expect(config.principal, equals(PresetEcualizador.flat));
|
|
expect(
|
|
logs.any((l) => l.startsWith('[PluriWave][persistencia]')),
|
|
isTrue,
|
|
reason:
|
|
'una lectura degradada del preset principal debe quedar '
|
|
'registrada (antes silenciosa)',
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'C6 nombresDispositivos: sobrevive parcial con 2 nombres validos + 1 '
|
|
'valor corrupto',
|
|
() async {
|
|
final raw = jsonEncode({
|
|
'bt_a2dp:AA:BB': 'Living Room BT',
|
|
'wired_headset': 'Office Headset',
|
|
'builtin_speaker': 12345, // not a String -> must be skipped
|
|
});
|
|
SharedPreferences.setMockInitialValues({
|
|
'eq_nombres_dispositivos_v1': raw,
|
|
});
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final servicio = ServicioEcualizador(prefs: prefs);
|
|
|
|
final config = await servicio.cargar();
|
|
|
|
expect(
|
|
config.nombresDispositivos,
|
|
equals({
|
|
'bt_a2dp:AA:BB': 'Living Room BT',
|
|
'wired_headset': 'Office Headset',
|
|
}),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|