add CompositeDiscoveryService merging ENPC and SPRT results
Claude-Session: https://claude.ai/code/session_01E6N3P4nSZLtp4BC2LXVref
This commit is contained in:
118
Inspectron.Epson.Tests/CompositeDiscoveryServiceTests.cs
Normal file
118
Inspectron.Epson.Tests/CompositeDiscoveryServiceTests.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using Inspectron.Epson;
|
||||
|
||||
namespace Inspectron.Epson.Tests;
|
||||
|
||||
public class CompositeDiscoveryServiceTests
|
||||
{
|
||||
private sealed class StubDiscovery : IDiscoveryService
|
||||
{
|
||||
private readonly List<DiscoveredPrinter> _printers;
|
||||
private readonly Exception? _throws;
|
||||
|
||||
public StubDiscovery(params string[] ips)
|
||||
{
|
||||
_printers = ips.Select(ip => new DiscoveredPrinter { IPAddress = ip }).ToList();
|
||||
}
|
||||
|
||||
public StubDiscovery(Exception throws)
|
||||
{
|
||||
_printers = new List<DiscoveredPrinter>();
|
||||
_throws = throws;
|
||||
}
|
||||
|
||||
public Task<List<DiscoveredPrinter>> DiscoverPrintersAsync(
|
||||
TimeSpan? timeout = null,
|
||||
Action<DiscoveredPrinter>? onPrinterDiscovered = null)
|
||||
{
|
||||
if (_throws is not null)
|
||||
{
|
||||
throw _throws;
|
||||
}
|
||||
|
||||
foreach (var p in _printers)
|
||||
{
|
||||
onPrinterDiscovered?.Invoke(p);
|
||||
}
|
||||
|
||||
return Task.FromResult(_printers);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Merges_results_from_every_child()
|
||||
{
|
||||
var composite = new CompositeDiscoveryService(new IDiscoveryService[]
|
||||
{
|
||||
new StubDiscovery("10.0.0.1"),
|
||||
new StubDiscovery("10.0.0.2", "10.0.0.3")
|
||||
});
|
||||
|
||||
var result = await composite.DiscoverPrintersAsync(TimeSpan.FromMilliseconds(50));
|
||||
|
||||
Assert.Equal(3, result.Count);
|
||||
Assert.Contains(result, p => p.IPAddress == "10.0.0.1");
|
||||
Assert.Contains(result, p => p.IPAddress == "10.0.0.2");
|
||||
Assert.Contains(result, p => p.IPAddress == "10.0.0.3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Deduplicates_by_ip_first_wins()
|
||||
{
|
||||
// A printer answering both ENPC and SPRT must appear once.
|
||||
var composite = new CompositeDiscoveryService(new IDiscoveryService[]
|
||||
{
|
||||
new StubDiscovery("10.0.0.1"),
|
||||
new StubDiscovery("10.0.0.1")
|
||||
});
|
||||
|
||||
var result = await composite.DiscoverPrintersAsync(TimeSpan.FromMilliseconds(50));
|
||||
|
||||
Assert.Single(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task One_child_throwing_does_not_suppress_the_others()
|
||||
{
|
||||
var composite = new CompositeDiscoveryService(new IDiscoveryService[]
|
||||
{
|
||||
new StubDiscovery(new InvalidOperationException("boom")),
|
||||
new StubDiscovery("10.0.0.7")
|
||||
});
|
||||
|
||||
var result = await composite.DiscoverPrintersAsync(TimeSpan.FromMilliseconds(50));
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal("10.0.0.7", result[0].IPAddress);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Forwards_the_discovered_callback()
|
||||
{
|
||||
var seen = new List<string>();
|
||||
var composite = new CompositeDiscoveryService(new IDiscoveryService[]
|
||||
{
|
||||
new StubDiscovery("10.0.0.1", "10.0.0.2")
|
||||
});
|
||||
|
||||
await composite.DiscoverPrintersAsync(TimeSpan.FromMilliseconds(50),
|
||||
p => seen.Add(p.IPAddress));
|
||||
|
||||
Assert.Equal(2, seen.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Callback_fires_once_per_ip_even_when_two_children_report_it()
|
||||
{
|
||||
var seen = new List<string>();
|
||||
var composite = new CompositeDiscoveryService(new IDiscoveryService[]
|
||||
{
|
||||
new StubDiscovery("10.0.0.1"),
|
||||
new StubDiscovery("10.0.0.1")
|
||||
});
|
||||
|
||||
await composite.DiscoverPrintersAsync(TimeSpan.FromMilliseconds(50),
|
||||
p => seen.Add(p.IPAddress));
|
||||
|
||||
Assert.Single(seen);
|
||||
}
|
||||
}
|
||||
62
Inspectron.Epson/CompositeDiscoveryService.cs
Normal file
62
Inspectron.Epson/CompositeDiscoveryService.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Inspectron.Epson;
|
||||
|
||||
/// <summary>
|
||||
/// Runs several discovery services concurrently and merges their results by IP.
|
||||
///
|
||||
/// Epson printers answer ENPC (DiscoveryService) and SPRT-family printers answer
|
||||
/// SPRT (SprtDiscoveryService); both broadcast on UDP 3289. A child that fails is
|
||||
/// logged and skipped so one broken protocol cannot blind the others.
|
||||
/// </summary>
|
||||
public class CompositeDiscoveryService : IDiscoveryService
|
||||
{
|
||||
private readonly IReadOnlyList<IDiscoveryService> _services;
|
||||
private readonly ILogger? _logger;
|
||||
|
||||
public CompositeDiscoveryService(IEnumerable<IDiscoveryService> services, ILogger? logger = null)
|
||||
{
|
||||
_services = services.ToList();
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<List<DiscoveredPrinter>> DiscoverPrintersAsync(
|
||||
TimeSpan? timeout = null,
|
||||
Action<DiscoveredPrinter>? onPrinterDiscovered = null)
|
||||
{
|
||||
var merged = new List<DiscoveredPrinter>();
|
||||
var seenIps = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Children run concurrently, so the caller's timeout still bounds the
|
||||
// whole pass rather than the sum of the children.
|
||||
var tasks = _services.Select(async service =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return await service.DiscoverPrintersAsync(timeout);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Discovery service {Service} failed",
|
||||
service.GetType().Name);
|
||||
return new List<DiscoveredPrinter>();
|
||||
}
|
||||
});
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
|
||||
// Task.WhenAll has completed, so this merge is single-threaded.
|
||||
foreach (var printer in results.SelectMany(r => r))
|
||||
{
|
||||
if (printer.IPAddress is null || !seenIps.Add(printer.IPAddress))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
merged.Add(printer);
|
||||
onPrinterDiscovered?.Invoke(printer);
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user