diff --git a/Inspectron.Epson.Tests/Telemetry/InsinTelemetryDiscoveredPrintersReceiverTests.cs b/Inspectron.Epson.Tests/Telemetry/InsinTelemetryDiscoveredPrintersReceiverTests.cs new file mode 100644 index 0000000..526eab9 --- /dev/null +++ b/Inspectron.Epson.Tests/Telemetry/InsinTelemetryDiscoveredPrintersReceiverTests.cs @@ -0,0 +1,104 @@ +using EpsonPrintService; +using Inspectron.Epson; +using Inspectron.Epson.PrintServer.Telemetry; + +namespace Inspectron.Epson.Tests.Telemetry; + +public class InsinTelemetryDiscoveredPrintersReceiverTests +{ + private sealed class RecordingTelemetry : IInsinTelemetry + { + public List<(string Kind, string Message)> Events { get; } = new(); + public void Emit(string kind, string message) => Events.Add((kind, message)); + } + + private sealed class CountingInner : IDiscoveredPrintersReceiver + { + public int Calls; + public Task OnPrintersDiscoveredAsync(IReadOnlyList printers) + { + Calls++; + return Task.CompletedTask; + } + } + + private static DiscoveredPrinter Printer(string ip, string model = "TM-T30III") => + new() { IPAddress = ip, ModelName = model }; + + [Fact] + public async Task First_sighting_emits_discovered_and_online() + { + var telemetry = new RecordingTelemetry(); + var inner = new CountingInner(); + var sut = new InsinTelemetryDiscoveredPrintersReceiver(inner, telemetry); + + await sut.OnPrintersDiscoveredAsync(new[] { Printer("192.168.1.10") }); + + Assert.Equal(1, inner.Calls); + Assert.Collection(telemetry.Events, + e => Assert.Equal("printer.discovered", e.Kind), + e => Assert.Equal("printer.online", e.Kind)); + Assert.Contains("printer=192.168.1.10", telemetry.Events[0].Message); + Assert.Contains("model=TM-T30III", telemetry.Events[0].Message); + } + + [Fact] + public async Task Repeat_sighting_emits_nothing() + { + var telemetry = new RecordingTelemetry(); + var sut = new InsinTelemetryDiscoveredPrintersReceiver(new CountingInner(), telemetry); + await sut.OnPrintersDiscoveredAsync(new[] { Printer("192.168.1.10") }); + telemetry.Events.Clear(); + + await sut.OnPrintersDiscoveredAsync(new[] { Printer("192.168.1.10") }); + + Assert.Empty(telemetry.Events); + } + + [Fact] + public async Task Disappearance_emits_offline() + { + var telemetry = new RecordingTelemetry(); + var sut = new InsinTelemetryDiscoveredPrintersReceiver(new CountingInner(), telemetry); + await sut.OnPrintersDiscoveredAsync(new[] { Printer("192.168.1.10") }); + telemetry.Events.Clear(); + + await sut.OnPrintersDiscoveredAsync(Array.Empty()); + + Assert.Single(telemetry.Events); + Assert.Equal("printer.offline", telemetry.Events[0].Kind); + Assert.Equal("printer=192.168.1.10", telemetry.Events[0].Message); + } + + [Fact] + public async Task Reappearance_emits_online_only_not_discovered() + { + var telemetry = new RecordingTelemetry(); + var sut = new InsinTelemetryDiscoveredPrintersReceiver(new CountingInner(), telemetry); + await sut.OnPrintersDiscoveredAsync(new[] { Printer("192.168.1.10") }); + await sut.OnPrintersDiscoveredAsync(Array.Empty()); + telemetry.Events.Clear(); + + await sut.OnPrintersDiscoveredAsync(new[] { Printer("192.168.1.10") }); + + Assert.Single(telemetry.Events); + Assert.Equal("printer.online", telemetry.Events[0].Kind); + } + + [Fact] + public async Task Telemetry_failures_do_not_break_inner() + { + var telemetry = new ThrowingTelemetry(); + var inner = new CountingInner(); + var sut = new InsinTelemetryDiscoveredPrintersReceiver(inner, telemetry); + + await sut.OnPrintersDiscoveredAsync(new[] { Printer("192.168.1.10") }); + + Assert.Equal(1, inner.Calls); + } + + private sealed class ThrowingTelemetry : IInsinTelemetry + { + public void Emit(string kind, string message) => throw new InvalidOperationException("boom"); + } +} diff --git a/Inspectron.Epson/PrintServer/DiscoveredPrintersReceiver/InsinTelemetryDiscoveredPrintersReceiver.cs b/Inspectron.Epson/PrintServer/DiscoveredPrintersReceiver/InsinTelemetryDiscoveredPrintersReceiver.cs new file mode 100644 index 0000000..e52dc92 --- /dev/null +++ b/Inspectron.Epson/PrintServer/DiscoveredPrintersReceiver/InsinTelemetryDiscoveredPrintersReceiver.cs @@ -0,0 +1,71 @@ +using Inspectron.Epson; +using Inspectron.Epson.PrintServer.Telemetry; + +namespace EpsonPrintService; + +public sealed class InsinTelemetryDiscoveredPrintersReceiver : IDiscoveredPrintersReceiver +{ + private readonly IDiscoveredPrintersReceiver _inner; + private readonly IInsinTelemetry _telemetry; + private readonly HashSet _seenIps = new(); + private readonly Dictionary _present = new(); + private readonly object _stateLock = new(); + + public InsinTelemetryDiscoveredPrintersReceiver( + IDiscoveredPrintersReceiver inner, + IInsinTelemetry telemetry) + { + _inner = inner; + _telemetry = telemetry; + } + + public async Task OnPrintersDiscoveredAsync(IReadOnlyList printers) + { + await _inner.OnPrintersDiscoveredAsync(printers); + + List<(string kind, string message)> toEmit; + lock (_stateLock) + { + toEmit = ComputeEvents(printers); + } + + foreach (var (kind, message) in toEmit) + { + try { _telemetry.Emit(kind, message); } + catch { /* swallow: telemetry must never break discovery */ } + } + } + + private List<(string, string)> ComputeEvents(IReadOnlyList printers) + { + var events = new List<(string, string)>(); + var currentIps = new HashSet(); + foreach (var p in printers) + { + if (string.IsNullOrEmpty(p.IPAddress)) continue; + currentIps.Add(p.IPAddress); + + if (_seenIps.Add(p.IPAddress)) + events.Add((InsinEventKinds.PrinterDiscovered, + InsinMessageFormatter.Format(("printer", p.IPAddress), ("model", p.ModelName)))); + + if (!_present.TryGetValue(p.IPAddress, out var wasPresent) || !wasPresent) + events.Add((InsinEventKinds.PrinterOnline, + InsinMessageFormatter.Format(("printer", p.IPAddress)))); + + _present[p.IPAddress] = true; + } + + foreach (var kv in _present.ToList()) + { + if (kv.Value && !currentIps.Contains(kv.Key)) + { + events.Add((InsinEventKinds.PrinterOffline, + InsinMessageFormatter.Format(("printer", kv.Key)))); + _present[kv.Key] = false; + } + } + + return events; + } +}