257 lines
8.3 KiB
C#
257 lines
8.3 KiB
C#
using System.Diagnostics;
|
|
using Omika.App.Local.Abstractions;
|
|
using Omika.App.Local.Services;
|
|
|
|
namespace Omika.Tests.Mobile.Local;
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
public class BackgroundSyncServiceTests
|
|
{
|
|
/// <summary>Connectivité simulée, avec comptage des interrogations.</summary>
|
|
private sealed class CountingNetwork : INetworkConnectivity
|
|
{
|
|
private int _probes;
|
|
|
|
public bool Online { get; set; } = true;
|
|
|
|
public int Probes => Volatile.Read(ref _probes);
|
|
|
|
public Task<bool> 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<CancellationToken, Task<int>> pending = _ => Task.FromResult(1);
|
|
Func<CancellationToken, Task> 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));
|
|
}
|
|
}
|