diff --git a/NAPS2.Sdk.Tests/NAPS2.Sdk.Tests.csproj b/NAPS2.Sdk.Tests/NAPS2.Sdk.Tests.csproj index b079fbe16f..1045c8bef3 100644 --- a/NAPS2.Sdk.Tests/NAPS2.Sdk.Tests.csproj +++ b/NAPS2.Sdk.Tests/NAPS2.Sdk.Tests.csproj @@ -28,6 +28,7 @@ + diff --git a/NAPS2.Sdk.Tests/Remoting/ScanServerIntegrationTests.cs b/NAPS2.Sdk.Tests/Remoting/ScanServerIntegrationTests.cs new file mode 100644 index 0000000000..aa5ffb4a63 --- /dev/null +++ b/NAPS2.Sdk.Tests/Remoting/ScanServerIntegrationTests.cs @@ -0,0 +1,166 @@ +using Microsoft.Extensions.Logging; +using NAPS2.Escl.Server; +using NAPS2.Remoting.Server; +using NAPS2.Scan; +using NAPS2.Scan.Exceptions; +using NAPS2.Scan.Internal; +using NAPS2.Sdk.Tests.Asserts; +using NAPS2.Sdk.Tests.Mocks; +using NSubstitute; +using Xunit; +using Xunit.Abstractions; + +namespace NAPS2.Sdk.Tests.Remoting; + +public class ScanServerIntegrationTests : ContextualTests +{ + private readonly ScanServer _server; + private readonly MockScanBridge _bridge; + private readonly ScanController _client; + private readonly ScanDevice _clientDevice; + + public ScanServerIntegrationTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + _server = new ScanServer(ScanningContext, new EsclServer()); + + // Set up a server connecting to a mock scan backend + _bridge = new MockScanBridge + { + MockOutput = [CreateScannedImage()] + }; + var scanBridgeFactory = Substitute.For(); + scanBridgeFactory.Create(Arg.Any()).Returns(_bridge); + _server.ScanController = new ScanController(ScanningContext, scanBridgeFactory); + + // Initialize the server with a single device with a unique ID for the test + var displayName = $"testName{Guid.NewGuid()}"; + ScanningContext.Logger.LogDebug("Display name: {Name}", displayName); + var serverDevice = new ScanDevice(ScanOptionsValidator.SystemDefaultDriver, "testID", "testName"); + var serverSharedDevice = new SharedDevice { Device = serverDevice, Name = displayName }; + _server.RegisterDevice(serverSharedDevice); + _server.Start().Wait(); + + // Set up a client ScanController for scanning through EsclScanDriver -> network -> ScanServer + _client = new ScanController(ScanningContext); + // This device won't match exactly the real device from GetDeviceList but it includes the UUID which is enough + // for EsclScanDriver to correctly identify the server for scanning. + _clientDevice = new ScanDevice(Driver.Escl, $"|{serverSharedDevice.GetUuid(_server.InstanceId)}", displayName); + } + + public override void Dispose() + { + _server.Dispose(); + base.Dispose(); + } + + [Fact] + public async Task FindDevice() + { + var devices = await _client.GetDeviceList(Driver.Escl); + // The device name is suffixed with the IP so we just check the prefix matches (and vice versa for ID) + Assert.Contains(devices, + device => device.Name.StartsWith(_clientDevice.Name) && device.ID.EndsWith(_clientDevice.ID)); + } + + [Fact] + public async Task Scan() + { + var images = await _client.Scan(new ScanOptions + { + Device = _clientDevice + }).ToListAsync(); + Assert.Single(images); + ImageAsserts.Similar(ImageResources.dog, images[0]); + } + + [Fact] + public async Task ScanMultiplePages() + { + _bridge.MockOutput = + CreateScannedImages(ImageResources.dog, ImageResources.dog_h_n300, ImageResources.dog_h_p300).ToList(); + var images = await _client.Scan(new ScanOptions + { + Device = _clientDevice, + PaperSource = PaperSource.Feeder + }).ToListAsync(); + Assert.Equal(3, images.Count); + ImageAsserts.Similar(ImageResources.dog, images[0]); + ImageAsserts.Similar(ImageResources.dog_h_n300, images[1]); + ImageAsserts.Similar(ImageResources.dog_h_p300, images[2]); + } + + [Fact] + public async Task ScanWithCorrectOptions() + { + var images = await _client.Scan(new ScanOptions + { + Device = _clientDevice, + BitDepth = BitDepth.Color, + Dpi = 100, + PaperSource = PaperSource.Flatbed, + PageSize = PageSize.Letter, + PageAlign = HorizontalAlign.Right + }).ToListAsync(); + + var opts = _bridge.LastOptions; + Assert.Equal(BitDepth.Color, opts.BitDepth); + Assert.Equal(100, opts.Dpi); + Assert.Equal(PaperSource.Flatbed, opts.PaperSource); + Assert.Equal(PageSize.Letter, opts.PageSize); + Assert.Equal(HorizontalAlign.Right, opts.PageAlign); + Assert.Single(images); + ImageAsserts.Similar(ImageResources.dog, images[0]); + + _bridge.MockOutput = CreateScannedImages(ImageResources.dog_gray).ToList(); + images = await _client.Scan(new ScanOptions + { + Device = _clientDevice, + BitDepth = BitDepth.Grayscale, + Dpi = 300, + PaperSource = PaperSource.Feeder, + PageSize = PageSize.Legal, + PageAlign = HorizontalAlign.Center + }).ToListAsync(); + + opts = _bridge.LastOptions; + Assert.Equal(BitDepth.Grayscale, opts.BitDepth); + Assert.Equal(300, opts.Dpi); + Assert.Equal(PaperSource.Feeder, opts.PaperSource); + Assert.Equal(PageSize.Legal, opts.PageSize); + Assert.Equal(HorizontalAlign.Center, opts.PageAlign); + Assert.Single(images); + ImageAsserts.Similar(ImageResources.dog_gray, images[0]); + + _bridge.MockOutput = CreateScannedImages(ImageResources.dog_bw).ToList(); + images = await _client.Scan(new ScanOptions + { + Device = _clientDevice, + BitDepth = BitDepth.BlackAndWhite, + Dpi = 4800, + PaperSource = PaperSource.Duplex, + PageSize = PageSize.A3, + PageAlign = HorizontalAlign.Left + }).ToListAsync(); + + opts = _bridge.LastOptions; + Assert.Equal(BitDepth.BlackAndWhite, opts.BitDepth); + Assert.Equal(4800, opts.Dpi); + Assert.Equal(PaperSource.Duplex, opts.PaperSource); + Assert.Equal(PageSize.A3.WidthInMm, opts.PageSize!.WidthInMm, 1); + Assert.Equal(PageSize.A3.HeightInMm, opts.PageSize!.HeightInMm, 1); + Assert.Equal(HorizontalAlign.Left, opts.PageAlign); + Assert.Single(images); + ImageAsserts.Similar(ImageResources.dog_bw, images[0]); + } + + [Fact] + public async Task ScanWithError() + { + _bridge.Error = new NoPagesException(); + + await Assert.ThrowsAsync(async () => await _client.Scan(new ScanOptions + { + Device = _clientDevice + }).ToListAsync()); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Remoting/Server/ScanServer.cs b/NAPS2.Sdk/Remoting/Server/ScanServer.cs index c000272a71..309bcf1769 100644 --- a/NAPS2.Sdk/Remoting/Server/ScanServer.cs +++ b/NAPS2.Sdk/Remoting/Server/ScanServer.cs @@ -43,7 +43,7 @@ public void RegisterDevice(SharedDevice sharedDevice) } public void UnregisterDevice(ScanDevice device, string? displayName = null) => - UnregisterDevice(new SharedDevice { Device = device, Name = displayName ?? device.Name, Port = 0 }); + UnregisterDevice(new SharedDevice { Device = device, Name = displayName ?? device.Name }); public void UnregisterDevice(SharedDevice sharedDevice) { @@ -82,7 +82,7 @@ private EsclDeviceConfig MakeEsclDeviceConfig(SharedDevice device) }; } - public void Start() => _esclServer.Start(); + public Task Start() => _esclServer.Start(); public void Stop() => _esclServer.Stop(); diff --git a/NAPS2.Sdk/Remoting/Server/SharedDevice.cs b/NAPS2.Sdk/Remoting/Server/SharedDevice.cs index 96768fe8a3..8f3c4b4311 100644 --- a/NAPS2.Sdk/Remoting/Server/SharedDevice.cs +++ b/NAPS2.Sdk/Remoting/Server/SharedDevice.cs @@ -8,7 +8,7 @@ public record SharedDevice { public required string Name { get; init; } public required ScanDevice Device { get; init; } - public required int Port { get; init; } + public int Port { get; init; } public string GetUuid(Guid instanceId) { diff --git a/NAPS2.Sdk/Scan/ScanController.cs b/NAPS2.Sdk/Scan/ScanController.cs index 9c5d79b47e..f5e1d149de 100644 --- a/NAPS2.Sdk/Scan/ScanController.cs +++ b/NAPS2.Sdk/Scan/ScanController.cs @@ -38,6 +38,12 @@ public ScanController(ScanningContext scanningContext, OcrController ocrControll { } + internal ScanController(ScanningContext scanningContext, IScanBridgeFactory scanBridgeFactory) + : this(scanningContext, new LocalPostProcessor(scanningContext, new OcrController(scanningContext)), + new ScanOptionsValidator(), scanBridgeFactory) + { + } + internal ScanController(ScanningContext scanningContext, ILocalPostProcessor localPostProcessor, ScanOptionsValidator scanOptionsValidator, IScanBridgeFactory scanBridgeFactory) {