using Omika.Core.Configuration; using Omika.Core.Observability; namespace Omika.Tests.Observability; public class TelemetryWorkerTests { [Fact] public async Task Worker_DrainsToExporter() { var (queue, counters) = CreateQueue(); var exporter = new CollectingExporter(); var worker = new TelemetryWorker(queue, exporter); queue.TryEnqueue(ObservabilityTestHarness.NewEvent(TelemetryEventCodes.MobileAppStarted)); queue.TryEnqueue(ObservabilityTestHarness.NewEvent(TelemetryEventCodes.MobileAppStarted)); using var cts = new CancellationTokenSource(); Task run = worker.ExecuteAsync(cts.Token); await ObservabilityTestHarness.WaitUntilAsync(() => exporter.Count == 2); cts.Cancel(); await run; } [Fact] public async Task Worker_DoesNotStarveLowPriority() { var (queue, _) = CreateQueue(); var exporter = new CollectingExporter(); var worker = new TelemetryWorker(queue, exporter); // Fort arriéré de P0 + quelques P4 : les P4 doivent quand même être exportés. for (int i = 0; i < 100; i++) { queue.TryEnqueue(ObservabilityTestHarness.NewEvent( TelemetryEventCodes.MobileSyncOperationReceived, priority: TelemetryPriority.P0)); } for (int i = 0; i < 10; i++) { queue.TryEnqueue(ObservabilityTestHarness.NewEvent( TelemetryEventCodes.MobileAppStarted, priority: TelemetryPriority.P4)); } using var cts = new CancellationTokenSource(); Task run = worker.ExecuteAsync(cts.Token); await ObservabilityTestHarness.WaitUntilAsync(() => exporter.Count == 110); cts.Cancel(); await run; Assert.Equal(100, exporter.ByPriority(TelemetryPriority.P0)); Assert.Equal(10, exporter.ByPriority(TelemetryPriority.P4)); } [Fact] public async Task Worker_ExporterThrows_DoesNotPropagate() { var (queue, _) = CreateQueue(); var worker = new TelemetryWorker(queue, new ThrowingExporter()); queue.TryEnqueue(ObservabilityTestHarness.NewEvent(TelemetryEventCodes.MobileAppStarted)); using var cts = new CancellationTokenSource(); Task run = worker.ExecuteAsync(cts.Token); // Laisse au worker le temps de tenter l'export, d'échouer et de reprendre. await Task.Delay(300); cts.Cancel(); await run; // Le lot a été abandonné (perte de log acceptable) ; aucune exception. Assert.Equal(0, queue.Count); } [Fact] public async Task Worker_Cancellation_Stops() { var (queue, _) = CreateQueue(); var worker = new TelemetryWorker(queue, new NullTelemetryExporter()); using var cts = new CancellationTokenSource(); Task run = worker.ExecuteAsync(cts.Token); cts.Cancel(); await run; } private static (PriorityTelemetryQueue Queue, TelemetryCounters Counters) CreateQueue() { var options = new ObservabilityOptions(); var counters = new TelemetryCounters(); return (new PriorityTelemetryQueue(options, counters), counters); } private sealed class CollectingExporter : ITelemetryExporter { private readonly object _lock = new(); private readonly List _exported = []; public int Count { get { lock (_lock) { return _exported.Count; } } } public int ByPriority(TelemetryPriority priority) { lock (_lock) { return _exported.Count(e => e.EffectivePriority == priority); } } public Task ExportAsync(IReadOnlyList events, CancellationToken cancellationToken = default) { lock (_lock) { _exported.AddRange(events); } return Task.CompletedTask; } } private sealed class ThrowingExporter : ITelemetryExporter { public Task ExportAsync(IReadOnlyList events, CancellationToken cancellationToken = default) => throw new InvalidOperationException("exporteur défaillant (test)"); } }