Files
Print_server/Inspectron.Epson.Tests/CompositeDiscoveryServiceTests.cs
EugeneTes 40a72195d3 strengthen CompositeDiscoveryService tests for deduplication and concurrency
- Replace weak deduplication test with one using distinguishable printers
  (TM-T30III/ENPC vs POS895/SPRT on same IP) to prove first-wins semantics
- Add concurrency test using SlowStubDiscovery to catch sequential regressions
  (3 * 300ms tasks must complete in < 700ms, not >= 900ms)
- Add StubDiscovery constructor accepting List<DiscoveredPrinter> for
  fully-populated test data

Test suite now 13/13 passing with proven concurrent execution.

Claude-Session: https://claude.ai/code/session_01E6N3P4nSZLtp4BC2LXVref
2026-09-03 12:03:24 +02:00

177 lines
5.8 KiB
C#

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 StubDiscovery(List<DiscoveredPrinter> printers)
{
_printers = printers;
}
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);
}
}
private sealed class SlowStubDiscovery : IDiscoveryService
{
private readonly TimeSpan _delay;
private readonly string _ip;
public SlowStubDiscovery(TimeSpan delay, string ip)
{
_delay = delay;
_ip = ip;
}
public async Task<List<DiscoveredPrinter>> DiscoverPrintersAsync(
TimeSpan? timeout = null,
Action<DiscoveredPrinter>? onPrinterDiscovered = null)
{
await Task.Delay(_delay);
return new List<DiscoveredPrinter> { new DiscoveredPrinter { IPAddress = _ip } };
}
}
[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_keeping_the_first_child_that_reported_it()
{
// The two stubs report the same IP but are distinguishable, so this
// proves first-wins rather than merely proving that one of them survived.
var first = new StubDiscovery(new List<DiscoveredPrinter>
{
new DiscoveredPrinter { IPAddress = "10.0.0.1", ModelName = "TM-T30III", ServiceType = "ENPC" }
});
var second = new StubDiscovery(new List<DiscoveredPrinter>
{
new DiscoveredPrinter { IPAddress = "10.0.0.1", ModelName = "POS895", ServiceType = "SPRT" }
});
var composite = new CompositeDiscoveryService(new IDiscoveryService[] { first, second });
var result = await composite.DiscoverPrintersAsync(TimeSpan.FromMilliseconds(50));
var only = Assert.Single(result);
Assert.Equal("TM-T30III", only.ModelName);
Assert.Equal("ENPC", only.ServiceType);
}
[Fact]
public async Task Runs_children_concurrently_not_sequentially()
{
// Three children that each take ~300ms. Sequential execution needs >=900ms;
// concurrent should land near 300ms. The 700ms bound leaves generous headroom
// for a loaded machine while still failing a sequential regression widely.
var children = new IDiscoveryService[]
{
new SlowStubDiscovery(TimeSpan.FromMilliseconds(300), "10.0.0.1"),
new SlowStubDiscovery(TimeSpan.FromMilliseconds(300), "10.0.0.2"),
new SlowStubDiscovery(TimeSpan.FromMilliseconds(300), "10.0.0.3")
};
var composite = new CompositeDiscoveryService(children);
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var result = await composite.DiscoverPrintersAsync(TimeSpan.FromSeconds(5));
stopwatch.Stop();
Assert.Equal(3, result.Count);
Assert.True(stopwatch.ElapsedMilliseconds < 700,
$"expected concurrent execution near 300ms but took {stopwatch.ElapsedMilliseconds}ms, " +
"which indicates the children ran sequentially");
}
[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);
}
}