using System.Diagnostics;
using Omika.App.Local.Abstractions;
using Omika.App.Local.Services;
namespace Omika.Tests.Mobile.Local;
///
/// Tests de la surveillance de fond de la file Outbox : les données saisies
/// localement doivent partir automatiquement dès que le réseau est disponible,
/// sans action de l'utilisateur (la synchronisation manuelle reste disponible).
///
public class BackgroundSyncServiceTests
{
/// Connectivité simulée, avec comptage des interrogations.
private sealed class CountingNetwork : INetworkConnectivity
{
private int _probes;
public bool Online { get; set; } = true;
public int Probes => Volatile.Read(ref _probes);
public Task IsOnlineAsync(CancellationToken cancellationToken = default)
{
Interlocked.Increment(ref _probes);
return Task.FromResult(Online);
}
}
private static readonly TimeSpan Court = TimeSpan.FromMilliseconds(20);
[Fact]
public async Task CycleAutomatique_QuandDonneesEnAttenteEtReseauDisponible()
{
var network = new CountingNetwork();
var service = new BackgroundSyncService(network, interval: Court);
int pending = 1;
int cycles = 0;
var premierCycle = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
service.Start(
cancellationToken => Task.FromResult(Volatile.Read(ref pending)),
_ =>
{
Interlocked.Increment(ref cycles);
// La file est vidée par le cycle (comportement réel : les
// opérations envoyées quittent l'état « en attente »).
Volatile.Write(ref pending, 0);
premierCycle.TrySetResult();
return Task.CompletedTask;
});
await premierCycle.Task.WaitAsync(TimeSpan.FromSeconds(5));
// Plus rien en attente : la surveillance ne relance aucun cycle.
await Task.Delay(200);
Assert.Equal(1, Volatile.Read(ref cycles));
await service.StopAsync();
}
[Fact]
public async Task AucunCycle_HorsLigne()
{
var network = new CountingNetwork { Online = false };
var service = new BackgroundSyncService(network, interval: Court);
int cycles = 0;
service.Start(
_ => Task.FromResult(1),
_ =>
{
Interlocked.Increment(ref cycles);
return Task.CompletedTask;
});
// Plusieurs tours ont lieu (données en attente) : aucun cycle réseau.
await Task.Delay(250);
await service.StopAsync();
Assert.Equal(0, Volatile.Read(ref cycles));
Assert.True(network.Probes > 0, "la connectivité doit être vérifiée à chaque tour");
}
[Fact]
public async Task AucunCycle_SansDonneesEnAttente_EtAucuneInterrogationReseau()
{
var network = new CountingNetwork();
var service = new BackgroundSyncService(network, interval: Court);
int cycles = 0;
service.Start(
_ => Task.FromResult(0),
_ =>
{
Interlocked.Increment(ref cycles);
return Task.CompletedTask;
});
await Task.Delay(250);
await service.StopAsync();
Assert.Equal(0, Volatile.Read(ref cycles));
// Rien à envoyer : inutile d'interroger le réseau (économie batterie).
Assert.Equal(0, network.Probes);
}
[Fact]
public async Task RequestSync_DeclencheLeCycleSansAttendreLeDelaiPeriodique()
{
var network = new CountingNetwork();
// Délai périodique très long : seul le réveil explicite peut déclencher.
var service = new BackgroundSyncService(network, interval: TimeSpan.FromMinutes(10));
int cycles = 0;
var cycle = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
service.Start(
_ => Task.FromResult(1),
_ =>
{
Interlocked.Increment(ref cycles);
cycle.TrySetResult();
return Task.CompletedTask;
});
// Écriture locale (ex. création d'un compte rendu) : réveil immédiat.
service.RequestSync();
var sw = Stopwatch.StartNew();
await cycle.Task.WaitAsync(TimeSpan.FromSeconds(5));
sw.Stop();
await service.StopAsync();
Assert.Equal(1, Volatile.Read(ref cycles));
Assert.True(sw.Elapsed < TimeSpan.FromSeconds(5), $"cycle déclenché en {sw.Elapsed}");
}
[Fact]
public async Task UnCycleEnErreurNArretePasLaSurveillance()
{
var network = new CountingNetwork();
var service = new BackgroundSyncService(network, interval: Court);
int cycles = 0;
var deuxiemeCycle = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
service.Start(
_ => Task.FromResult(1),
_ =>
{
if (Interlocked.Increment(ref cycles) == 1)
{
throw new InvalidOperationException("échec réseau simulé");
}
deuxiemeCycle.TrySetResult();
return Task.CompletedTask;
});
// La boucle survit à l'échec et réessaie d'elle-même.
await deuxiemeCycle.Task.WaitAsync(TimeSpan.FromSeconds(5));
await service.StopAsync();
Assert.True(Volatile.Read(ref cycles) >= 2, $"cycles observés : {Volatile.Read(ref cycles)}");
}
[Fact]
public async Task RepliExponentiel_QuandLaFileNeProgressePas()
{
var network = new CountingNetwork();
var service = new BackgroundSyncService(
network,
interval: TimeSpan.FromMilliseconds(40),
maxBackoff: TimeSpan.FromMilliseconds(320));
int cycles = 0;
// La file ne diminue jamais (opération bloquée) : la surveillance doit
// espacer ses tentatives au lieu de marteler le serveur.
service.Start(
_ => Task.FromResult(5),
_ =>
{
Interlocked.Increment(ref cycles);
return Task.CompletedTask;
});
await Task.Delay(1000);
await service.StopAsync();
int observed = Volatile.Read(ref cycles);
Assert.True(observed >= 2, $"au moins deux tentatives attendues, observées : {observed}");
// Sans repli, ~25 cycles seraient exécutés sur la même fenêtre.
Assert.True(observed <= 8, $"repli exponentiel attendu, cycles observés : {observed}");
}
[Fact]
public async Task Arret_StoppeLaSurveillance()
{
var network = new CountingNetwork();
var service = new BackgroundSyncService(network, interval: Court);
int cycles = 0;
service.Start(
_ => Task.FromResult(1),
_ =>
{
Interlocked.Increment(ref cycles);
return Task.CompletedTask;
});
await Task.Delay(150);
await service.StopAsync();
int apresArret = Volatile.Read(ref cycles);
await Task.Delay(200);
Assert.Equal(apresArret, Volatile.Read(ref cycles));
}
[Fact]
public async Task DemarrageIdempotent_UneSeuleBoucleDeSurveillance()
{
var network = new CountingNetwork();
var service = new BackgroundSyncService(network, interval: Court);
int concurrent = 0;
int maximum = 0;
Func> pending = _ => Task.FromResult(1);
Func action = async _ =>
{
int enCours = Interlocked.Increment(ref concurrent);
if (enCours > Volatile.Read(ref maximum))
{
Volatile.Write(ref maximum, enCours);
}
await Task.Delay(60);
Interlocked.Decrement(ref concurrent);
};
// Deux démarrages simultanés ne doivent pas ouvrir deux boucles qui
// exécuteraient deux cycles en parallèle.
service.Start(pending, action);
service.Start(pending, action);
await Task.Delay(400);
await service.StopAsync();
Assert.Equal(1, Volatile.Read(ref maximum));
}
}