Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user