diff --git a/Mosey.sln b/Mosey.sln
index 32a3f0e..72ee011 100644
--- a/Mosey.sln
+++ b/Mosey.sln
@@ -6,6 +6,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mosey", "Mosey\Mosey.csproj
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3D609DE7-CA92-45B0-A4A9-93975FA50AA9}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mosey.Tests", "MoseyTests\Mosey.Tests.csproj", "{A6FA9705-788E-4712-BE02-E13D8A652020}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -28,6 +30,18 @@ Global
{B1DD8B7D-FFF0-406C-9765-14848EAB126F}.Release|x64.Build.0 = Release|Any CPU
{B1DD8B7D-FFF0-406C-9765-14848EAB126F}.Release|x86.ActiveCfg = Release|Any CPU
{B1DD8B7D-FFF0-406C-9765-14848EAB126F}.Release|x86.Build.0 = Release|Any CPU
+ {A6FA9705-788E-4712-BE02-E13D8A652020}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A6FA9705-788E-4712-BE02-E13D8A652020}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A6FA9705-788E-4712-BE02-E13D8A652020}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A6FA9705-788E-4712-BE02-E13D8A652020}.Debug|x64.Build.0 = Debug|Any CPU
+ {A6FA9705-788E-4712-BE02-E13D8A652020}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A6FA9705-788E-4712-BE02-E13D8A652020}.Debug|x86.Build.0 = Debug|Any CPU
+ {A6FA9705-788E-4712-BE02-E13D8A652020}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A6FA9705-788E-4712-BE02-E13D8A652020}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A6FA9705-788E-4712-BE02-E13D8A652020}.Release|x64.ActiveCfg = Release|Any CPU
+ {A6FA9705-788E-4712-BE02-E13D8A652020}.Release|x64.Build.0 = Release|Any CPU
+ {A6FA9705-788E-4712-BE02-E13D8A652020}.Release|x86.ActiveCfg = Release|Any CPU
+ {A6FA9705-788E-4712-BE02-E13D8A652020}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Mosey/App.xaml.cs b/Mosey/App.xaml.cs
index 02096eb..d80e5bb 100644
--- a/Mosey/App.xaml.cs
+++ b/Mosey/App.xaml.cs
@@ -1,6 +1,8 @@
using System;
using System.IO;
+using System.IO.Abstractions;
using System.Windows;
+using System.Runtime.CompilerServices;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -12,6 +14,7 @@
using Mosey.Services.Imaging;
using Mosey.ViewModels;
+[assembly: InternalsVisibleTo("Mosey.Tests")]
namespace Mosey
{
///
@@ -60,9 +63,11 @@ protected override void OnStartup(StartupEventArgs e)
})
// Services
- .AddTransient()
+ .AddSingleton()
+ .AddTransient, IntervalTimerFactory>()
.AddTransient()
.AddScoped()
+ .AddTransient()
.AddTransient()
.AddSingleton, ScanningDevices>()
diff --git a/Mosey/Configuration/AppSettings.cs b/Mosey/Configuration/AppSettings.cs
index 34171f9..ed8f9f7 100644
--- a/Mosey/Configuration/AppSettings.cs
+++ b/Mosey/Configuration/AppSettings.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Text;
using Mosey.Models;
using Mosey.Services;
using Mosey.Services.Imaging;
diff --git a/Mosey/Models/Device.cs b/Mosey/Models/Device.cs
index 0be0860..8898c32 100644
--- a/Mosey/Models/Device.cs
+++ b/Mosey/Models/Device.cs
@@ -13,13 +13,6 @@ public interface IDeviceCollection where T : IDevice
///
IEnumerable Devices { get; }
- ///
- /// Retrieve the devices in the collection where their property is equal to .
- ///
- /// The specified property
- /// Devices where the is equal to
- IEnumerable GetByEnabled(bool enabled);
-
///
/// Add a to the collection.
///
@@ -36,13 +29,6 @@ public interface IDeviceCollection where T : IDevice
///
void DisableAll();
- ///
- /// Set the property of a specific instance.
- ///
- /// A instance
- /// Sets the property
- void SetDeviceEnabled(T device, bool enabled);
-
///
/// Set the property of a device in the collection.
///
diff --git a/Mosey/Models/FileSystemExtensions.cs b/Mosey/Models/FileSystemExtensions.cs
index 53a94a0..8cffd95 100644
--- a/Mosey/Models/FileSystemExtensions.cs
+++ b/Mosey/Models/FileSystemExtensions.cs
@@ -1,6 +1,5 @@
using System;
-using System.Collections.Generic;
-using System.IO;
+using System.IO.Abstractions;
using System.Linq;
namespace Mosey.Models
@@ -14,9 +13,9 @@ public static class FileSystemExtensions
/// A instance that represents the logical drive
///
///
- public static DriveInfo GetDriveInfo(string driveName)
+ public static IDriveInfo GetDriveInfo(string driveName, IFileSystem fileSystem)
{
- return DriveInfo.GetDrives().Where(drive => drive.Name == driveName).FirstOrDefault();
+ return fileSystem.DriveInfo.GetDrives().Where(drive => drive.Name == driveName).FirstOrDefault();
}
///
@@ -26,9 +25,9 @@ public static DriveInfo GetDriveInfo(string driveName)
/// The available free space, in bytes
///
///
- public static long AvailableFreeSpace(string driveName)
+ public static long AvailableFreeSpace(string driveName, IFileSystem fileSystem)
{
- return GetDriveInfo(driveName).AvailableFreeSpace;
+ return GetDriveInfo(driveName, fileSystem).AvailableFreeSpace;
}
///
@@ -36,15 +35,15 @@ public static long AvailableFreeSpace(string driveName)
///
/// The path to verify
/// if is a UNC path
- public static bool IsNetworkPath(string path)
+ public static bool IsNetworkPath(string path, IFileSystem fileSystem)
{
if (!path.StartsWith(@"/") && !path.StartsWith(@"\"))
{
// Path may not start with a slash, but could be a network drive
- string rootPath = Path.GetPathRoot(path);
- DriveInfo driveInfo = new DriveInfo(rootPath);
+ string rootPath = fileSystem.Path.GetPathRoot(path);
+ var driveInfo = fileSystem.DriveInfo.FromDriveName(rootPath);
- return driveInfo.DriveType == DriveType.Network;
+ return driveInfo.DriveType == System.IO.DriveType.Network;
}
return true;
diff --git a/Mosey/Mosey.csproj b/Mosey/Mosey.csproj
index ce28654..ac647d7 100644
--- a/Mosey/Mosey.csproj
+++ b/Mosey/Mosey.csproj
@@ -3,6 +3,7 @@
WinExe
netcoreapp3.1
+ 9.0
true
Mosey.ico
@@ -46,6 +47,7 @@
+
diff --git a/Mosey/Services/Imaging/Extensions/ImageFormatExtensions.cs b/Mosey/Services/Imaging/Extensions/ImageFormatExtensions.cs
index edfcf28..238298a 100644
--- a/Mosey/Services/Imaging/Extensions/ImageFormatExtensions.cs
+++ b/Mosey/Services/Imaging/Extensions/ImageFormatExtensions.cs
@@ -41,7 +41,7 @@ public static ImageFormat ToDrawingImageFormat(this ScanningDevice.ImageFormat v
/// Convert to a instance.
///
///
- /// A instance
+ /// A instance
public static DNTScanner.Core.WiaImageFormat ToWIAImageFormat(this ScanningDevice.ImageFormat value)
{
return (DNTScanner.Core.WiaImageFormat)typeof(DNTScanner.Core.WiaImageFormat)
diff --git a/Mosey/Services/Imaging/ISystemDevices.cs b/Mosey/Services/Imaging/ISystemDevices.cs
new file mode 100644
index 0000000..7bc3973
--- /dev/null
+++ b/Mosey/Services/Imaging/ISystemDevices.cs
@@ -0,0 +1,51 @@
+using System.Collections.Generic;
+using DNTScanner.Core;
+using Mosey.Models;
+
+namespace Mosey.Services.Imaging
+{
+ ///
+ /// Provides access to the WIA driver devices via DNTScanner.Core
+ ///
+ public interface ISystemDevices
+ {
+ ///
+ /// Retrieve image(s) from a scanner.
+ ///
+ /// A instance representing a physical device
+ /// Device settings used when capturing an image
+ /// The image format used internally for storing the image
+ /// A list of retrieved images as byte arrays, in
+ IEnumerable PerformScan(ScannerSettings settings, IImagingDeviceConfig config, ScanningDevice.ImageFormat format);
+
+ ///
+ /// The number of attempts to try connecting to the WIA driver, after
+ /// The time in millseconds between attempts
+ IEnumerable PerformScan(ScannerSettings settings, IImagingDeviceConfig config, ScanningDevice.ImageFormat format, int connectRetries, int delay);
+
+ ///
+ /// Lists the static properties of scanners connected to the system.
+ ///
+ /// Use the function to retrieve full device instances.
+ ///
+ ///
+ /// Static device properties are limited, but can be retrieved without establishing a connection to the device.
+ ///
+ /// A list of the static device properties
+ public IList> ScannerProperties();
+
+ ///
+ /// The number of retry attempts allowed if connecting to the WIA driver was unsuccessful
+ public IList> ScannerProperties(int connectRetries);
+
+ ///
+ /// A collection of representing physical devices connected to the system.
+ ///
+ /// A collection of representing physical devices connected to the system.
+ public IEnumerable ScannerSettings();
+
+ ///
+ /// The number of retry attempts allowed if connecting to the WIA driver was unsuccessful
+ public IEnumerable ScannerSettings(int connectRetries);
+ }
+}
diff --git a/Mosey/Services/Imaging/ScanningDevice.cs b/Mosey/Services/Imaging/ScanningDevice.cs
index 1ad73ea..af00825 100644
--- a/Mosey/Services/Imaging/ScanningDevice.cs
+++ b/Mosey/Services/Imaging/ScanningDevice.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Drawing.Imaging;
using System.IO;
+using System.IO.Abstractions;
using System.Linq;
using System.Runtime.InteropServices;
@@ -78,26 +79,35 @@ public enum ImageFormat
private bool _isEnabled;
private bool _isImaging;
private int _scanRetries = 5;
- private ScannerSettings _scannerSettings;
+ private readonly IFileSystem _fileSystem;
+ private readonly ScannerSettings _scannerSettings;
+ private readonly ISystemDevices _systemDevices;
///
/// Initialize a new instance using a instance that represents a physical scanner.
///
/// A instance representing a physical device
- public ScanningDevice(ScannerSettings settings)
- {
- _scannerSettings = settings;
- }
+ public ScanningDevice(ScannerSettings settings) : this(settings, null, null, null) { }
+
+ ///
+ /// Initialize a new instance using a instance that represents a physical scanner.
+ ///
+ /// A instance representing a physical device
+ /// Device settings used when capturing an image
+ public ScanningDevice(ScannerSettings settings, IImagingDeviceConfig config) : this(settings, config, null, null) { }
///
/// Initialize a new instance using a instance that represents a physical scanner.
///
/// A instance representing a physical device
/// Device settings used when capturing an image
- public ScanningDevice(ScannerSettings settings, IImagingDeviceConfig config)
+ /// An instance that provide access to the WIA driver devices
+ public ScanningDevice(ScannerSettings settings, IImagingDeviceConfig config, ISystemDevices systemDevices, IFileSystem fileSystem)
{
_scannerSettings = settings;
ImageSettings = config;
+ _systemDevices = systemDevices ?? new SystemDevices();
+ _fileSystem = fileSystem ?? new FileSystem();
}
public void ClearImages()
@@ -113,12 +123,12 @@ public void GetImage()
///
/// Retrieve an image from the physical imaging device.
///
- /// The image format used internally for storing the image
+ /// The image transfer format used when capturing the image
/// If the scanner is not connected
/// If the is not supported by the device
/// If the operation fails during scanning
///
- /// Images are converted to before being stored as byte arrays.
+ /// Images are converted to , if possible, before being stored as byte arrays.
///
public void GetImage(ImageFormat format)
{
@@ -144,7 +154,7 @@ public void GetImage(ImageFormat format)
IsImaging = true;
try
{
- var images = SystemDevices.PerformScan(_scannerSettings, deviceConfig, format);
+ var images = _systemDevices.PerformScan(_scannerSettings, deviceConfig, format);
// Remove any existing images
ClearImages();
@@ -152,9 +162,17 @@ public void GetImage(ImageFormat format)
// Store images for processing etc
foreach (var image in images)
{
- // Convert image to PNG format before storing byte array
- // Greatly reduces memory footprint compared to raw BMP
- Images.Add(image.AsFormat(ImageFormat.Png.ToDrawingImageFormat()));
+ try
+ {
+ // Convert image to PNG format before storing byte array
+ // Greatly reduces memory footprint compared to raw BMP
+ Images.Add(image.AsFormat(ImageFormat.Png.ToDrawingImageFormat()));
+ }
+ catch (ArgumentException)
+ {
+ // Store the image in its original format
+ Images.Add(image);
+ }
}
}
catch (Exception ex) when (ex is COMException || ex is InvalidOperationException)
@@ -175,7 +193,7 @@ public void GetImage(ImageFormat format)
///
public void SaveImage()
{
- string directory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures).ToString(), System.Reflection.Assembly.GetExecutingAssembly().GetName().Name);
+ string directory = _fileSystem.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures).ToString(), System.Reflection.Assembly.GetExecutingAssembly().GetName().Name);
SaveImage("image", directory, ImageFormat.Png);
}
@@ -195,16 +213,18 @@ public IEnumerable SaveImage(string fileName, string directory, string f
}
///
- /// Write an image captured with to disk
- /// Images are stored losslessly (in supported formats) with a colour depth of 24 bit per pixel
+ /// Write an image captured with to disk.
+ /// Images are stored losslessly (in supported formats) with a colour depth of 24 bit per pixel.
///
/// The image file name, the file extension ignored and instead inferred from
/// The directory path to use when storing the image
/// The used to store the image
/// A collection of file path s for the newly created images
+ /// If is or whitespace
+ /// If the property returns no images to save
public IEnumerable SaveImage(string fileName, string directory, ImageFormat imageFormat = ImageFormat.Png)
{
- if (Images == null | Images.Count == 0)
+ if (Images == null || Images.Count == 0)
{
throw new InvalidOperationException($"No images available. Please call the {nameof(GetImage)} method first.");
}
@@ -214,33 +234,26 @@ public IEnumerable SaveImage(string fileName, string directory, ImageFor
}
// Get full filename and path
- Directory.CreateDirectory(directory);
- fileName = Path.Combine(directory, fileName);
- fileName = Path.ChangeExtension(fileName, imageFormat.ToString().ToLower());
+ _fileSystem.Directory.CreateDirectory(directory);
+ fileName = _fileSystem.Path.Combine(directory, fileName);
+ fileName = _fileSystem.Path.ChangeExtension(fileName, imageFormat.ToString().ToLower());
// Use lossless compression with highest quality
- using (EncoderParameters encoderParameters = new EncoderParameters().AddParams(
+ using (var encoderParameters = new EncoderParameters().AddParams(
compression: EncoderValue.CompressionLZW,
quality: 100,
colorDepth: 24
))
{
- // Write all images to disk
- foreach (var imageBytes in Images)
- {
- imageBytes.ToImage().Save(
- fileName,
- imageFormat.ToDrawingImageFormat().CodecInfo(),
- encoderParameters
- );
- yield return fileName;
- }
+ // Write image(s) to disk using specified encoding
+ // Ensure we enumerate here otherwise the encoderParameters will go out of context
+ return SaveImagesToDisk(Images, fileName, imageFormat, encoderParameters).ToList();
}
}
public bool Equals(IImagingDevice device)
{
- return null != device && DeviceID == device.DeviceID;
+ return device is not null && DeviceID == device.DeviceID;
}
public override bool Equals(object obj)
@@ -253,6 +266,58 @@ public override int GetHashCode()
return DeviceID.GetHashCode();
}
+ ///
+ /// Store image byte arrays to disk.
+ ///
+ ///
+ /// The image count is appended to the filename in case of multiple images.
+ ///
+ /// An image byte array
+ /// The full file path used to store the images
+ /// The used to store the images
+ /// Specify image encoding when writing the images
+ /// A collection of file path s for the newly created images
+ protected internal IEnumerable SaveImagesToDisk(IEnumerable images, string filePath, ImageFormat format = ImageFormat.Png, EncoderParameters encoderParams = null)
+ {
+ int count = 1;
+ string fileName = Path.GetFileName(filePath);
+
+ // Write all images to disk
+ foreach (var imageBytes in Images)
+ {
+ // Append count to filename in case of multiple images
+ string savePath = filePath;
+ if (images.Count() > 1)
+ savePath = savePath.Replace(
+ fileName,
+ $"{Path.GetFileNameWithoutExtension(fileName)}_{count}{Path.GetExtension(filePath)}");
+
+ SaveImageToDisk(imageBytes, savePath, format, encoderParams);
+
+ yield return savePath;
+
+ count++;
+ }
+ }
+
+ ///
+ /// Store an image byte array to disk
+ ///
+ /// An image byte array
+ /// The full file path used to store the image
+ /// The used to store the image
+ /// Specify image encoding when writing the image
+ protected internal virtual void SaveImageToDisk(byte[] image, string filePath, ImageFormat format = ImageFormat.Png, EncoderParameters encoderParams = null)
+ {
+ using (var fileStream = _fileSystem.File.Create(filePath))
+ {
+ image.ToImage().Save(
+ fileStream,
+ format.ToDrawingImageFormat().CodecInfo(),
+ encoderParams);
+ }
+ }
+
///
/// A simplified version of the unique device identifier,
///
diff --git a/Mosey/Services/Imaging/ScanningDeviceSettings.cs b/Mosey/Services/Imaging/ScanningDeviceSettings.cs
index ca42485..2074de8 100644
--- a/Mosey/Services/Imaging/ScanningDeviceSettings.cs
+++ b/Mosey/Services/Imaging/ScanningDeviceSettings.cs
@@ -1,16 +1,13 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using DNTScanner.Core;
+using DNTScanner.Core;
using Mosey.Models;
-using Mosey.Services.Imaging.Extensions;
+using System;
namespace Mosey.Services.Imaging
{
///
/// Device settings used by a when capturing an image.
///
- public class ScanningDeviceSettings : IImagingDeviceConfig
+ public class ScanningDeviceSettings : IImagingDeviceConfig, IEquatable
{
public ImageColorFormat ColorFormat { get; set; } = ImageColorFormat.Color;
public int Resolution { get; set; }
@@ -38,8 +35,19 @@ public ScanningDeviceSettings(ImageColorFormat colorFormat, int resolution, int
}
public object Clone()
+ => MemberwiseClone();
+
+ public override bool Equals(object obj)
+ => Equals(obj as ScanningDeviceSettings);
+
+ public bool Equals(ScanningDeviceSettings other)
{
- return MemberwiseClone();
+ return other is not null &&
+ (ColorFormat, Resolution, Brightness, Contrast).Equals(
+ (other.ColorFormat, other.Resolution, other.Brightness, other.Contrast));
}
+
+ public override int GetHashCode()
+ => (ColorFormat, Resolution, Brightness, Contrast).GetHashCode();
}
}
diff --git a/Mosey/Services/Imaging/ScanningDevices.cs b/Mosey/Services/Imaging/ScanningDevices.cs
index 922b0f3..543ceaf 100644
--- a/Mosey/Services/Imaging/ScanningDevices.cs
+++ b/Mosey/Services/Imaging/ScanningDevices.cs
@@ -1,9 +1,9 @@
using System;
using System.Linq;
using System.Collections.Generic;
-using System.Threading;
using Microsoft.Extensions.Configuration;
using Mosey.Models;
+using DNTScanner.Core;
namespace Mosey.Services.Imaging
{
@@ -14,6 +14,9 @@ namespace Mosey.Services.Imaging
///
public class ScanningDevices : IImagingDevices
{
+ private readonly ICollection _devices = new ObservableItemsCollection();
+ private readonly ISystemDevices _systemDevices;
+
///
/// The number of time to attempt reconnection to the WIA driver.
///
@@ -22,25 +25,25 @@ public class ScanningDevices : IImagingDevices
///
/// A collection of s, representing physical scanners.
///
- public IEnumerable Devices { get { return _devices; } }
+ public IEnumerable Devices => _devices;
- public bool IsEmpty { get { return (_devices.Count == 0); } }
- public bool IsImagingInProgress { get { return _devices.Any(x => x.IsImaging is true); } }
- private ICollection _devices = new ObservableItemsCollection();
+ public bool IsEmpty => !_devices.Any();
+ public bool IsImagingInProgress => _devices.Any(x => x.IsImaging is true);
///
/// Initialize an empty collection.
///
- public ScanningDevices()
- {
- }
+ /// An instance that provide access to the WIA driver devices
+ public ScanningDevices(ISystemDevices systemDevices) : this(null, systemDevices) { }
///
/// Initialize the collection s with the specified .
///
/// Used to initialize the collection's s
- public ScanningDevices(IImagingDeviceConfig deviceConfig)
+ /// An instance that provide access to the WIA driver devices
+ public ScanningDevices(IImagingDeviceConfig deviceConfig, ISystemDevices systemDevices)
{
+ _systemDevices = systemDevices ?? new SystemDevices();
GetDevices(deviceConfig);
}
@@ -54,6 +57,8 @@ public ScanningDevice AddDevice(string deviceID)
return AddDevice(deviceID, null);
}
+ ///
+ /// If a device with the same already exists in the collection
public void AddDevice(IDevice device)
{
AddDevice((ScanningDevice)device);
@@ -68,7 +73,7 @@ public void AddDevice(ScanningDevice device)
{
if (!_devices.Contains(device))
{
- _devices.Add((IImagingDevice)device);
+ _devices.Add(device);
}
else
{
@@ -88,7 +93,7 @@ public ScanningDevice AddDevice(string deviceID, IImagingDeviceConfig config)
ScanningDevice device = null;
// Attempt to connect a device matching the deviceID
- var settings = SystemDevices.ScannerSettings(ConnectRetries).Where(x => x.Id == deviceID).FirstOrDefault();
+ var settings = _systemDevices.ScannerSettings(ConnectRetries).FirstOrDefault(x => x.Id == deviceID);
if (settings != null)
{
@@ -109,33 +114,25 @@ public void EnableAll()
SetByEnabled(true);
}
- public IEnumerable GetByEnabled(bool enabled)
- {
- return _devices.Where(x => x.IsEnabled == enabled).AsEnumerable();
- }
-
///
/// Retrieve s that are connected to the system and add them to the collection.
/// Update the status of any devices are already present in the collection.
///
public void RefreshDevices()
{
- // Get a new collection of devices if none already present
- if (IsEmpty)
- {
- GetDevices();
- }
- else
- {
- RefreshDevices(new ScanningDeviceSettings());
- }
+ RefreshDevices(new ScanningDeviceSettings());
}
- ///
+ ///
public void RefreshDevices(IImagingDeviceConfig deviceConfig, bool enableDevices = true)
{
- IList> deviceProperties = SystemDevices.ScannerProperties(connectRetries: ConnectRetries);
- if (deviceProperties.Count == 0)
+ const string DEVICE_ID_KEY = "Unique Device ID";
+ var deviceIds = new List();
+
+ // Check the static properties for changed IDs and only retrieve ScannerSetting instances if necessary.
+ // This saves connecting the devices via the WIA driver and is much faster
+ var deviceProperties = _systemDevices.ScannerProperties(connectRetries: ConnectRetries);
+ if (deviceProperties is null || !deviceProperties.Any())
{
// No devices detected, any current devices have been disconnected
foreach (ScanningDevice device in _devices)
@@ -147,62 +144,66 @@ public void RefreshDevices(IImagingDeviceConfig deviceConfig, bool enableDevices
// Check if devices not already in the collection
// Or already in the collection, but not connected
- foreach (IDictionary properties in deviceProperties)
+ var currentDevices = Devices.Select(d => d.DeviceID);
+ foreach (var properties in deviceProperties)
{
- string deviceID = properties["Unique Device ID"].ToString();
-
- if (!_devices.Where(d => d.DeviceID == deviceID).Any())
+ if (properties.TryGetValue(DEVICE_ID_KEY, out var id))
{
- // Create a new device and add it to the collection
- ScanningDevice device = AddDevice(deviceID, deviceConfig);
- device.IsEnabled = enableDevices;
- }
- else
- {
- ScanningDevice existingDevice = (ScanningDevice)_devices.Where(d => d.DeviceID == deviceID && !d.IsConnected).FirstOrDefault();
- if (existingDevice != null)
- {
- // Remove the existing device from the collection
- bool enabled = existingDevice.IsEnabled;
- _devices.Remove(existingDevice);
-
- // Replace with the new and updated device
- ScanningDevice device = AddDevice(deviceID, deviceConfig);
- device.IsEnabled = enabled;
- }
+ deviceIds.Add((string)id);
}
+ }
+
+ // These devices can no longer be found and are disconnected
+ foreach (var deviceId in currentDevices.Except(deviceIds))
+ {
+ var device = (ScanningDevice)_devices.FirstOrDefault(d => d.DeviceID == deviceId);
+ device.IsConnected = false;
+ }
+
+ // These are new devices, add them to the collection
+ foreach (var deviceId in deviceIds.Except(currentDevices))
+ {
+ var device = AddDevice(deviceId, deviceConfig);
+ device.IsEnabled = enableDevices;
+ }
- // If the device is in the collection but no longer found
- IEnumerable devicesRemoved = _devices.Where(l1 => !deviceProperties.Any(l2 => l1.DeviceID == l2["Unique Device ID"].ToString()));
- if (devicesRemoved.Count() > 0)
+ // These devices are already in the collection, but previously disconnected
+ foreach (var deviceId in currentDevices.Intersect(deviceIds))
+ {
+ var existingDevice = (ScanningDevice)_devices.FirstOrDefault(d => d.DeviceID == deviceId && !d.IsConnected);
+ if (existingDevice is not null)
{
- foreach (ScanningDevice device in devicesRemoved)
- {
- device.IsConnected = false;
- }
+ // Remove the existing device and replace with the updated
+ bool enabled = existingDevice.IsEnabled;
+ _devices.Remove(existingDevice);
+
+ var device = AddDevice(deviceId, deviceConfig);
+ device.IsEnabled = enabled;
}
}
}
public void SetDeviceEnabled(string deviceID, bool enabled)
{
- _devices.Where(x => x.DeviceID == deviceID).First().IsEnabled = enabled;
- }
-
- public void SetDeviceEnabled(IImagingDevice device, bool enabled)
- {
- _devices.Where(x => x.DeviceID == device.DeviceID).First().IsEnabled = enabled;
+ _devices.First(x => x.DeviceID == deviceID).IsEnabled = enabled;
}
///
- /// Retrieve s that are connected to the system and add them to the collection.
- /// The exisiting items in the collection are cleared first.
+ /// A collection of instances representing physical devices connected to the system.
///
- /// The number of devices added to the collection
- private int GetDevices()
+ /// Used to initialize the instances
+ /// A collection of instances representing physical scanning devices connected to the system
+ private IEnumerable ScannerDevices(IImagingDeviceConfig deviceConfig)
+ => ScannerDevices(deviceConfig, 1);
+
+ ///
+ /// The number of retry attempts allowed if connecting to the WIA driver was unsuccessful
+ private IEnumerable ScannerDevices(IImagingDeviceConfig deviceConfig, int connectRetries = 1)
{
- // Populate a new collection of scanners using default image settings
- return GetDevices(new ScanningDeviceSettings());
+ foreach (var settings in _systemDevices.ScannerSettings(connectRetries))
+ {
+ yield return new ScanningDevice(settings, deviceConfig);
+ }
}
///
@@ -214,11 +215,11 @@ private int GetDevices()
/// The number of devices added to the collection
private int GetDevices(IImagingDeviceConfig deviceConfig)
{
- // Empty the collection
- _devices.Clear();
+ deviceConfig ??= new ScanningDeviceSettings();
// Populate a new collection of scanners using specified image settings
- foreach (ScanningDevice device in SystemDevices.ScannerDevices(deviceConfig, ConnectRetries))
+ _devices.Clear();
+ foreach (ScanningDevice device in ScannerDevices(deviceConfig, ConnectRetries))
{
device.IsEnabled = true;
AddDevice(device);
diff --git a/Mosey/Services/Imaging/SystemDevices.cs b/Mosey/Services/Imaging/SystemDevices.cs
index 87aec71..3e6b5ae 100644
--- a/Mosey/Services/Imaging/SystemDevices.cs
+++ b/Mosey/Services/Imaging/SystemDevices.cs
@@ -12,22 +12,21 @@ namespace Mosey.Services.Imaging
///
/// Provides access to the WIA driver devices via DNTScanner.Core
///
- internal static class SystemDevices
+ internal sealed class SystemDevices : ISystemDevices
{
private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
- ///
- /// Retrieve image(s) from a scanner.
- ///
- /// A instance representing a physical device
- /// Device settings used when capturing an image
- /// The image format used internally for storing the image
- /// The number of attempts to try connecting to the WIA driver, after
- /// The time in millseconds between attempts
- /// A list of retrieved images as byte arrays, in
+ ///
+ /// If an error occurs within the specified number of
+ /// If an error occurs within the specified number of
+ public IEnumerable PerformScan(ScannerSettings settings, IImagingDeviceConfig config, ScanningDevice.ImageFormat format)
+ {
+ return PerformScan(settings, config, format, 1, 1000);
+ }
+ ///
/// If an error occurs within the specified number of
/// If an error occurs within the specified number of
- internal static IEnumerable PerformScan(ScannerSettings settings, IImagingDeviceConfig config, ScanningDevice.ImageFormat format, int connectRetries = 1, int delay = 1000)
+ public IEnumerable PerformScan(ScannerSettings settings, IImagingDeviceConfig config, ScanningDevice.ImageFormat format, int connectRetries, int delay)
{
IEnumerable images = new List();
@@ -41,95 +40,55 @@ internal static IEnumerable PerformScan(ScannerSettings settings, IImagi
return images;
}
- ///
- /// A collection of instances representing physical devices connected to the system.
- ///
- /// Used to initialize the instances
- /// The number of retry attempts allowed if connecting to the WIA driver was unsuccessful
- /// A semaphore to coordinate connections to the WIA driver
- /// A collection of instances representing physical scanning devices connected to the system
+ ///
/// If an error occurs within the specified number of
/// If an error occurs within the specified number of
- internal static IEnumerable ScannerDevices(IImagingDeviceConfig deviceConfig, int connectRetries = 1)
+ public IList> ScannerProperties()
{
- var devices = new List();
-
- foreach (var settings in ScannerSettings(connectRetries))
- {
- // Store the device in the collection
- devices.Add(new ScanningDevice(settings, deviceConfig));
- }
-
- return devices;
+ return ScannerProperties(1);
}
- ///
- /// Lists the static properties of scanners connected to the system.
- /// Use the function to retrieve full device instances.
- ///
- ///
- /// Static device properties are limited, but can be retrieved without establishing a connection to the device.
- ///
- /// The number of retry attempts allowed if connecting to the WIA driver was unsuccessful
- /// A list of the static device properties
+ ///
/// If an error occurs within the specified number of
/// If an error occurs within the specified number of
- internal static IList> ScannerProperties(int connectRetries = 1)
+ public IList> ScannerProperties(int connectRetries)
{
IList> properties = new List>();
properties = WIARetry(
DNTScanner.Core.SystemDevices.GetScannerDeviceProperties,
connectRetries,
- _semaphore
- );
+ _semaphore);
return properties;
}
- ///
- /// A collection of representing physical devices connected to the system.
- ///
- /// The number of retry attempts allowed if connecting to the WIA driver was unsuccessful
- /// A collection of representing physical devices connected to the system.
+ ///
/// If an error occurs within the specified number of
/// If an error occurs within the specified number of
- internal static IEnumerable ScannerSettings(int connectRetries = 1)
+ public IEnumerable ScannerSettings()
{
- var deviceList = new List();
+ return ScannerSettings(1);
+ }
- var systemScanners = WIARetry(
+ ///
+ /// If an error occurs within the specified number of
+ /// If an error occurs within the specified number of
+ public IEnumerable ScannerSettings(int connectRetries)
+ {
+ return WIARetry(
DNTScanner.Core.SystemDevices.GetScannerDevices,
connectRetries,
- _semaphore
- );
-
- // Check that at least one scanner can be found
- if (systemScanners.FirstOrDefault() == null)
- {
- return deviceList;
- }
-
- foreach (ScannerSettings settings in systemScanners)
- {
- // Store the device in the collection
- deviceList.Add(settings);
- }
-
- return deviceList;
+ _semaphore).AsEnumerable();
}
///
/// Create and configure a new instance.
///
- ///
- /// If the resolution specified in is not available,
- /// the closest value in will be used.
- ///
/// A instance representing a physical device
/// Device settings used when capturing an image
/// A instance configured using
- private static ScannerDevice ConfiguredScannerDevice(ScannerSettings settings, IImagingDeviceConfig config)
+ private ScannerDevice ConfiguredScannerDevice(ScannerSettings settings, IImagingDeviceConfig config)
{
var device = new ScannerDevice(settings);
var supportedResolutions = settings.SupportedResolutions;
@@ -183,6 +142,7 @@ private static void WIARetry(Action method, int connectRetries = 1, SemaphoreSli
/// The WIA driver will produce COMExceptions if attempting to connect to a device that is
/// not ready, cannot be found etc. In many cases a successful connection can be made by reattempting
/// shortly after.
+ ///
/// The DNTScanner wrapper does not provide any events or async methods that would allow for simply
/// awaiting the WIA driver directly
///
diff --git a/Mosey/Services/IntervalTimerFactory.cs b/Mosey/Services/IntervalTimerFactory.cs
new file mode 100644
index 0000000..b3375f7
--- /dev/null
+++ b/Mosey/Services/IntervalTimerFactory.cs
@@ -0,0 +1,12 @@
+using Mosey.Models;
+
+namespace Mosey.Services
+{
+ internal class IntervalTimerFactory : IFactory
+ {
+ public IIntervalTimer Create()
+ {
+ return new IntervalTimer();
+ }
+ }
+}
diff --git a/Mosey/ViewModels/MainViewModel.cs b/Mosey/ViewModels/MainViewModel.cs
index ac752ba..10d2ecc 100644
--- a/Mosey/ViewModels/MainViewModel.cs
+++ b/Mosey/ViewModels/MainViewModel.cs
@@ -11,19 +11,22 @@
using AsyncAwaitBestPractices.MVVM;
using Mosey.Models;
using Mosey.Configuration;
+using System.IO.Abstractions;
namespace Mosey.ViewModels
{
public class MainViewModel : ViewModelBase, IViewModelParent, IClosing, IDisposable
{
// From IoC container
- private readonly IIntervalTimer _scanTimer;
+ private readonly IFactory _timerFactory;
private readonly Services.UIServices _uiServices;
private readonly IViewModel _settingsViewModel;
private readonly IOptionsMonitor _appSettings;
+ private readonly IFileSystem _fileSystem;
private readonly ILogger _log;
// From constructor
+ private readonly IIntervalTimer _scanTimer;
private readonly IIntervalTimer _uiTimer;
private readonly DialogViewModel _dialog;
@@ -94,7 +97,7 @@ public long ImagesRequiredDiskSpace
get
{
return ScanRepetitions
- * ScanningDevices.GetByEnabled(true).Count()
+ * ScanningDevices.Devices.Count(d => d.IsEnabled)
* _userDeviceConfig.GetResolutionMetaData(_imageConfig.Resolution).FileSize;
}
}
@@ -266,22 +269,25 @@ public SettingsViewModel SettingsViewModel
#endregion Properties
public MainViewModel(
- IIntervalTimer intervalTimer,
+ IFactory intervalTimerFactory,
IImagingDevices imagingDevices,
Services.UIServices uiServices,
IViewModel settingsViewModel,
IOptionsMonitor appSettings,
+ IFileSystem fileSystem,
ILogger logger
)
{
- _scanTimer = intervalTimer;
+ _timerFactory = intervalTimerFactory;
ScanningDevices = imagingDevices;
_uiServices = uiServices;
_settingsViewModel = settingsViewModel;
_appSettings = appSettings;
+ _fileSystem = fileSystem;
_log = logger;
- _uiTimer = (IIntervalTimer)intervalTimer.Clone();
+ _scanTimer = intervalTimerFactory.Create();
+ _uiTimer = intervalTimerFactory.Create();
_dialog = new DialogViewModel(this, _uiServices, _log);
Initialize();
@@ -290,13 +296,13 @@ ILogger logger
public override IViewModel Create()
{
return new MainViewModel(
- intervalTimer: _scanTimer,
+ intervalTimerFactory: _timerFactory,
imagingDevices: ScanningDevices,
uiServices: _uiServices,
settingsViewModel: _settingsViewModel,
appSettings: _appSettings,
- logger: _log
- );
+ fileSystem: _fileSystem,
+ logger: _log);
}
#region Commands
@@ -439,7 +445,7 @@ private void Initialize()
System.Windows.Data.BindingOperations.EnableCollectionSynchronization(ScanningDevices.Devices, _scanningDevicesLock);
// Register event callbacks
- _appSettings.OnChange(UpdateConfig);
+ _appSettings.OnChange(UpdateConfig);
_scanTimer.Tick += ScanTimer_Tick;
_scanTimer.Complete += ScanTimer_Complete;
_uiTimer.Tick += UITimer_Tick;
@@ -456,7 +462,7 @@ private void Initialize()
/// Update local configuration from supplied .
///
/// Application configuration settings
- private void UpdateConfig(AppSettings settings)
+ internal void UpdateConfig(AppSettings settings)
{
if (!IsScanRunning)
{
@@ -488,12 +494,12 @@ public void StartScan()
}
///
- /// Begin repeated scanning, after first if checking interval time and free disk space are sufficient.
+ /// Begin repeated scanning, after first checking if checking interval time and free disk space are sufficient.
///
public async void StartScanWithDialog()
{
// Check that interval time is sufficient for selected resolution
- TimeSpan imagingTime = ScanningDevices.GetByEnabled(true).Count() * _userDeviceConfig.GetResolutionMetaData(_imageConfig.Resolution).ImagingTime;
+ var imagingTime = ScanningDevices.Devices.Count(d => d.IsEnabled) * _userDeviceConfig.GetResolutionMetaData(_imageConfig.Resolution).ImagingTime;
if (imagingTime * 1.5 > TimeSpan.FromMinutes(ScanInterval))
{
if (!await _dialog.ImagingTimeDialog(TimeSpan.FromMinutes(ScanInterval), imagingTime))
@@ -506,7 +512,9 @@ public async void StartScanWithDialog()
// Check that disk space is sufficient for selected resolution
try
{
- long availableDiskSpace = FileSystemExtensions.AvailableFreeSpace(Path.GetPathRoot(_imageFileConfig.Directory));
+ long availableDiskSpace = FileSystemExtensions.AvailableFreeSpace(
+ _fileSystem.Path.GetPathRoot(_imageFileConfig.Directory),
+ _fileSystem);
if (ImagesRequiredDiskSpace * 1.5 > availableDiskSpace)
{
if (!await _dialog.DiskSpaceDialog(ImagesRequiredDiskSpace, availableDiskSpace))
@@ -634,7 +642,7 @@ private void RefreshDevices()
/// The duration between refreshes
/// Used to stop the refresh loop
///
- private async Task RefreshDevicesAsync(int intervalSeconds = 1, CancellationToken cancellationToken = default)
+ internal async Task RefreshDevicesAsync(int intervalSeconds = 1, CancellationToken cancellationToken = default)
{
_log.LogDebug($"Device refresh initiated with {nameof(RefreshDevicesAsync)}");
while (true)
@@ -720,8 +728,8 @@ public List Scan(CancellationToken cancellationToken = default)
{
_log.LogDebug("{ImageCount} images retrieved from scanner #{DeviceID}", scanner.Images.Count(), scanner.ID);
string fileName = string.Join("_", _imageFileConfig.Prefix, saveDateTime);
- string directory = Path.Combine(saveDirectory, string.Join(string.Empty, "Scanner", scannerIDStr));
- Directory.CreateDirectory(directory);
+ string directory = _fileSystem.Path.Combine(saveDirectory, string.Join(string.Empty, "Scanner", scannerIDStr));
+ _fileSystem.Directory.CreateDirectory(directory);
// Write image(s) to filesystem and retrieve a list of saved file names
IEnumerable savedImages = scanner.SaveImage(fileName, directory: directory, fileFormat: ImageFormat);
@@ -805,7 +813,7 @@ await Task.Factory.StartNew(() =>
private void ImageDirectoryDialog()
{
// Go up one level so users can see the initial directory instead of starting inside it
- string initialDirectory = Directory.GetParent(ImageSavePath).FullName;
+ string initialDirectory = _fileSystem.Directory.GetParent(ImageSavePath).FullName;
if (string.IsNullOrWhiteSpace(initialDirectory)) initialDirectory = ImageSavePath;
string selectedDirectory = _dialog.FolderBrowserDialog(
diff --git a/Mosey/ViewModels/SettingsViewModel.cs b/Mosey/ViewModels/SettingsViewModel.cs
index eda9e8d..0152b04 100644
--- a/Mosey/ViewModels/SettingsViewModel.cs
+++ b/Mosey/ViewModels/SettingsViewModel.cs
@@ -6,16 +6,18 @@
using Mosey.Configuration;
using Mosey.Models;
using Mosey.Services;
+using System.IO.Abstractions;
namespace Mosey.ViewModels
{
public class SettingsViewModel : ViewModelBase
{
private ILogger _log;
- private readonly UIServices _uiServices;
private IWritableOptions _appSettings;
private AppSettings _userSettings;
+ private readonly UIServices _uiServices;
private readonly DialogViewModel _dialog;
+ private readonly IFileSystem _fileSystem;
#region Properties
public string ImageSavePath
@@ -168,14 +170,15 @@ public string Version
public SettingsViewModel(
ILogger logger,
UIServices uiServices,
- IWritableOptions appSettings
- )
+ IWritableOptions appSettings,
+ IFileSystem fileSystem)
{
_log = logger;
_uiServices = uiServices;
_appSettings = appSettings;
_userSettings = appSettings.Get("UserSettings");
_dialog = new DialogViewModel(this, _uiServices, _log);
+ _fileSystem = fileSystem;
}
public override IViewModel Create()
@@ -183,8 +186,8 @@ public override IViewModel Create()
return new SettingsViewModel(
logger: _log,
uiServices: _uiServices,
- appSettings: _appSettings
- );
+ appSettings: _appSettings,
+ fileSystem: _fileSystem);
}
#region Commands
@@ -246,7 +249,7 @@ private void ResetUserOptions()
private void ImageDirectoryDialog()
{
// Go up one level so users can see the initial directory instead of starting inside it
- string initialDirectory = Directory.GetParent(ImageSavePath).FullName;
+ string initialDirectory = _fileSystem.Directory.GetParent(ImageSavePath).FullName;
if (string.IsNullOrWhiteSpace(initialDirectory)) initialDirectory = ImageSavePath;
string selectedDirectory = _dialog.FolderBrowserDialog(
diff --git a/MoseyTests/AutoData/AutoNSubstituteDataAttribute.cs b/MoseyTests/AutoData/AutoNSubstituteDataAttribute.cs
new file mode 100644
index 0000000..ed667ee
--- /dev/null
+++ b/MoseyTests/AutoData/AutoNSubstituteDataAttribute.cs
@@ -0,0 +1,32 @@
+using System;
+using System.IO.Abstractions;
+using System.IO.Abstractions.TestingHelpers;
+using AutoFixture;
+using AutoFixture.AutoNSubstitute;
+using AutoFixture.NUnit3;
+
+namespace MoseyTests.AutoData
+{
+ public class AutoNSubstituteDataAttribute : AutoDataAttribute
+ {
+ public AutoNSubstituteDataAttribute(Action initialize) : base(() =>
+ {
+ var fixture = new Fixture();
+
+ fixture.Customize(new AutoNSubstituteCustomization()
+ {
+ ConfigureMembers = true,
+ GenerateDelegates = true
+ });
+
+ fixture.Register(() => new MockFileSystem());
+
+ initialize(fixture);
+
+ return fixture;
+ })
+ { }
+
+ public AutoNSubstituteDataAttribute() : this(fixture => { }) { }
+ }
+}
diff --git a/MoseyTests/AutoData/CollectionSizeAttribute.cs b/MoseyTests/AutoData/CollectionSizeAttribute.cs
new file mode 100644
index 0000000..687cdf9
--- /dev/null
+++ b/MoseyTests/AutoData/CollectionSizeAttribute.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using AutoFixture;
+using AutoFixture.Kernel;
+using AutoFixture.NUnit3;
+
+namespace MoseyTests.AutoData
+{
+ public class CollectionSizeAttribute : CustomizeAttribute
+ {
+ private readonly int _size;
+
+ public CollectionSizeAttribute(int size)
+ {
+ _size = size;
+ }
+
+ public override ICustomization GetCustomization(ParameterInfo parameter)
+ {
+ if (parameter == null) throw new ArgumentNullException(nameof(parameter));
+
+ var objectType = parameter.ParameterType.GetGenericArguments()[0];
+
+ var isTypeCompatible =
+ parameter.ParameterType.IsGenericType
+ && parameter.ParameterType.GetGenericTypeDefinition().MakeGenericType(objectType).IsAssignableFrom(typeof(List<>).MakeGenericType(objectType))
+ ;
+ if (!isTypeCompatible)
+ {
+ throw new InvalidOperationException($"{nameof(CollectionSizeAttribute)} specified for type incompatible with List: {parameter.ParameterType} {parameter.Name}");
+ }
+
+ var customizationType = typeof(CollectionSizeCustomization<>).MakeGenericType(objectType);
+ return (ICustomization)Activator.CreateInstance(customizationType, parameter, _size);
+ }
+
+ public class CollectionSizeCustomization : ICustomization
+ {
+ private readonly ParameterInfo _parameter;
+ private readonly int _repeatCount;
+
+ public CollectionSizeCustomization(ParameterInfo parameter, int repeatCount)
+ {
+ _parameter = parameter;
+ _repeatCount = repeatCount;
+ }
+
+ public void Customize(IFixture fixture)
+ {
+ fixture.Customizations.Add(new FilteringSpecimenBuilder(
+ new FixedBuilder(fixture.CreateMany(_repeatCount).ToList()),
+ new EqualRequestSpecification(_parameter)
+ ));
+ }
+ }
+ }
+}
diff --git a/MoseyTests/AutoData/MainViewModelAutoDataAttribute.cs b/MoseyTests/AutoData/MainViewModelAutoDataAttribute.cs
new file mode 100644
index 0000000..1f7b13b
--- /dev/null
+++ b/MoseyTests/AutoData/MainViewModelAutoDataAttribute.cs
@@ -0,0 +1,15 @@
+using AutoFixture;
+using Mosey.Models;
+using Mosey.Services;
+
+namespace MoseyTests.AutoData
+{
+ public class MainViewModelAutoDataAttribute : AutoNSubstituteDataAttribute
+ {
+ public MainViewModelAutoDataAttribute() : base(fixture =>
+ {
+ fixture.Register>(fixture.Create);
+ })
+ { }
+ }
+}
diff --git a/MoseyTests/AutoData/ScanningDeviceAutoDataAttribute.cs b/MoseyTests/AutoData/ScanningDeviceAutoDataAttribute.cs
new file mode 100644
index 0000000..7d82d4d
--- /dev/null
+++ b/MoseyTests/AutoData/ScanningDeviceAutoDataAttribute.cs
@@ -0,0 +1,18 @@
+using AutoFixture;
+using MoseyTests.Customizations;
+
+namespace MoseyTests.AutoData
+{
+ public class ScanningDeviceAutoDataAttribute : AutoNSubstituteDataAttribute
+ {
+ public ScanningDeviceAutoDataAttribute() : base(fixture =>
+ {
+ fixture.Customize(new CompositeCustomization(new ScannerSettingsCustomization()));
+ // A concrete class is required when retrieving devices from ISystemDevices
+ fixture.Customize(new ConcreteScanningDeviceCustomization());
+ fixture.Customize(new ImageBytesCustomization());
+ fixture.Customize(new SystemDevicesMockCustomization());
+ })
+ { }
+ }
+}
diff --git a/MoseyTests/Customizations/ConcreteScanningDeviceCustomization.cs b/MoseyTests/Customizations/ConcreteScanningDeviceCustomization.cs
new file mode 100644
index 0000000..ea17010
--- /dev/null
+++ b/MoseyTests/Customizations/ConcreteScanningDeviceCustomization.cs
@@ -0,0 +1,14 @@
+using AutoFixture;
+using Mosey.Models;
+using Mosey.Services.Imaging;
+
+namespace MoseyTests.Customizations
+{
+ public class ConcreteScanningDeviceCustomization : ICustomization
+ {
+ public void Customize(IFixture fixture)
+ {
+ fixture.Register(fixture.Create);
+ }
+ }
+}
diff --git a/MoseyTests/Customizations/ImageBytesCustomization.cs b/MoseyTests/Customizations/ImageBytesCustomization.cs
new file mode 100644
index 0000000..67ddeb1
--- /dev/null
+++ b/MoseyTests/Customizations/ImageBytesCustomization.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Drawing;
+using System.IO;
+using AutoFixture;
+
+namespace MoseyTests.Customizations
+{
+ public class ImageBytesCustomization : ICustomization
+ {
+ public void Customize(IFixture fixture)
+ {
+ fixture.Register(() => GetBitmapData());
+ }
+
+ internal static byte[] GetBitmapData(int width = 10, int height = 10)
+ {
+ var random = new Random();
+ byte[] bitmapData;
+ var image = new Bitmap(width, height);
+
+ image.SetPixel(random.Next(width), random.Next(height), Color.Black);
+
+ using (var memoryStream = new MemoryStream())
+ {
+ image.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Bmp);
+ bitmapData = memoryStream.ToArray();
+ }
+ return bitmapData;
+ }
+ }
+}
diff --git a/MoseyTests/Customizations/ScannerSettingsCustomization.cs b/MoseyTests/Customizations/ScannerSettingsCustomization.cs
new file mode 100644
index 0000000..5373537
--- /dev/null
+++ b/MoseyTests/Customizations/ScannerSettingsCustomization.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using System.Linq;
+using AutoFixture;
+using DNTScanner.Core;
+using Mosey.Services.Imaging;
+using Mosey.Services.Imaging.Extensions;
+
+namespace MoseyTests.Customizations
+{
+ public class ScannerSettingsCustomization : ICustomization
+ {
+ public static ScanningDevice.ImageFormat SupportedImageFormat { get; set; } = ScanningDevice.ImageFormat.Jpeg;
+
+ public void Customize(IFixture fixture)
+ {
+ fixture.Register(() =>
+ {
+ var scannerSettings = GetInstance(fixture);
+ // Ensure that there is at least one real SupportedTransferFormat
+ scannerSettings
+ .SupportedTransferFormats
+ .Add(SupportedImageFormat.ToWIAImageFormat().Value, "SupportedWIAImageFormat");
+
+ return scannerSettings;
+ });
+ }
+
+ ///
+ /// Creates a new instance manually to avoid recursion problems
+ ///
+ internal static ScannerSettings GetInstance(IFixture fixture)
+ => new ScannerSettings
+ {
+ Id = fixture.Create(),
+ Name = fixture.Create(),
+ IsAutomaticDocumentFeeder = fixture.Create(),
+ IsDuplex = fixture.Create(),
+ IsFlatbed = fixture.Create(),
+ SupportedResolutions = fixture.CreateMany().ToList(),
+ SupportedTransferFormats = fixture.Create>(),
+ SupportedEvents = fixture.Create>(),
+ ScannerDeviceSettings = fixture.Create>(),
+ ScannerPictureSettings = fixture.Create>()
+ };
+ }
+}
diff --git a/MoseyTests/Customizations/SystemDevicesMockCustomization.cs b/MoseyTests/Customizations/SystemDevicesMockCustomization.cs
new file mode 100644
index 0000000..3421ee4
--- /dev/null
+++ b/MoseyTests/Customizations/SystemDevicesMockCustomization.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using System.Linq;
+using AutoFixture;
+using DNTScanner.Core;
+using Moq;
+using Mosey.Models;
+using Mosey.Services.Imaging;
+
+namespace MoseyTests.Customizations
+{
+ public class SystemDevicesMockCustomization : ICustomization
+ {
+ public void Customize(IFixture fixture)
+ {
+ var systemDevices = fixture.Create>();
+ var settings = fixture.CreateMany();
+ var properties = fixture.Create>>();
+
+ foreach (var deviceID in settings.Select(s => s.Id))
+ {
+ properties.Add(new Dictionary() { { "Unique Device ID", deviceID } });
+ }
+
+ systemDevices
+ .Setup(x => x.PerformScan(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Returns(fixture.CreateMany());
+
+ systemDevices
+ .Setup(mock => mock.ScannerSettings(It.IsAny()))
+ .Returns(settings);
+
+ systemDevices
+ .Setup(mock => mock.ScannerProperties(It.IsAny()))
+ .Returns(properties);
+
+ fixture.Register(() => systemDevices);
+ }
+ }
+}
diff --git a/MoseyTests/Extensions/TestExtensions.cs b/MoseyTests/Extensions/TestExtensions.cs
new file mode 100644
index 0000000..1342d89
--- /dev/null
+++ b/MoseyTests/Extensions/TestExtensions.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Linq;
+using System.Linq.Expressions;
+using NUnit.Framework;
+
+namespace MoseyTests.Extensions
+{
+ public static class TestExtensions
+ {
+ ///
+ /// Assert that an object's properties have been set and are not default values.
+ ///
+ ///
+ /// An object instance to verify
+ ///
+ public static void AssertAllPropertiesAreNotDefault(this T objectToInspect, params Expression>[] getters)
+ {
+ var defaultProperties = getters.Where(f => f.Compile()(objectToInspect).Equals(default(T)));
+
+ if (defaultProperties.Any())
+ {
+ var commaSeparatedPropertiesNames = string.Join(", ", defaultProperties.Select(GetName));
+ Assert.Fail("Expected properties not to have default values: " + commaSeparatedPropertiesNames);
+ }
+ }
+
+ ///
+ /// Retrieve a property name as a string.
+ ///
+ ///
+ ///
+ /// A property name as a string
+ public static string GetName(Expression> exp)
+ {
+ // Return type is an object, so type cast expression will be added to value types
+ if (!(exp.Body is MemberExpression body))
+ {
+ var ubody = (UnaryExpression)exp.Body;
+ body = ubody.Operand as MemberExpression;
+ }
+
+ return body.Member.Name;
+ }
+ }
+}
diff --git a/MoseyTests/Mosey.Tests.csproj b/MoseyTests/Mosey.Tests.csproj
new file mode 100644
index 0000000..2a348bb
--- /dev/null
+++ b/MoseyTests/Mosey.Tests.csproj
@@ -0,0 +1,31 @@
+
+
+
+ netcoreapp3.1
+ 9.0
+ false
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MoseyTests/Services/Imaging/ConfigTests.cs b/MoseyTests/Services/Imaging/ConfigTests.cs
new file mode 100644
index 0000000..5e5f96c
--- /dev/null
+++ b/MoseyTests/Services/Imaging/ConfigTests.cs
@@ -0,0 +1,56 @@
+using FluentAssertions;
+using NUnit.Framework;
+using AutoFixture.NUnit3;
+using MoseyTests.AutoData;
+using MoseyTests.Extensions;
+
+namespace Mosey.Services.Imaging.Tests
+{
+ public class ImageFileConfigTests
+ {
+ public class CloneShould
+ {
+ [Theory, AutoNSubstituteData]
+ public void BeEquivalentToClone(ImageFileConfig originalConfig)
+ {
+ var clonedConfig = originalConfig.Clone();
+
+ // Not a deep clone
+ originalConfig.Should().NotBeSameAs(clonedConfig);
+ clonedConfig.Should().BeEquivalentTo(originalConfig);
+ }
+ }
+ }
+
+ public class ScanningDeviceSettingsTests
+ {
+ public class ConstructorShould
+ {
+ [Theory, AutoNSubstituteData]
+ public void InitializeAllProperties(ScanningDeviceSettings sut)
+ {
+ sut.AssertAllPropertiesAreNotDefault();
+ }
+
+ [Theory, AutoNSubstituteData]
+ public void InitializeAllPropertiesWithGreedy([Greedy] ScanningDeviceSettings sut)
+ {
+ // AutoFixture uses least greedy constructor by default
+ sut.AssertAllPropertiesAreNotDefault();
+ }
+ }
+
+ public class CloneShould
+ {
+ [Theory, AutoNSubstituteData]
+ public void BeEquivalent(ScanningDeviceSettings originalSettings)
+ {
+ var clonedSettings = originalSettings.Clone();
+
+ // Not a deep clone
+ originalSettings.Should().NotBeSameAs(clonedSettings);
+ clonedSettings.Should().BeEquivalentTo(originalSettings);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/MoseyTests/Services/Imaging/ScanningDeviceTests.cs b/MoseyTests/Services/Imaging/ScanningDeviceTests.cs
new file mode 100644
index 0000000..a0cf203
--- /dev/null
+++ b/MoseyTests/Services/Imaging/ScanningDeviceTests.cs
@@ -0,0 +1,203 @@
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.IO.Abstractions;
+using System.IO.Abstractions.TestingHelpers;
+using FluentAssertions;
+using NUnit.Framework;
+using AutoFixture.NUnit3;
+using DNTScanner.Core;
+using MoseyTests.AutoData;
+using MoseyTests.Customizations;
+using MoseyTests.Extensions;
+
+namespace Mosey.Services.Imaging.Tests
+{
+ public class ScanningDeviceTests
+ {
+ public class ConstructorShould
+ {
+ [Theory, ScanningDeviceAutoData]
+ public void InitializeAllProperties(ScanningDevice sut)
+ {
+ sut.AssertAllPropertiesAreNotDefault();
+ sut.IsConnected.Should().BeTrue();
+ sut.IsImaging.Should().BeFalse();
+ }
+
+ [Theory, ScanningDeviceAutoData]
+ public void InitializeAllPropertiesGreedy([Greedy] ScanningDevice sut)
+ {
+ sut.AssertAllPropertiesAreNotDefault();
+ sut.IsConnected.Should().BeTrue();
+ sut.IsImaging.Should().BeFalse();
+ }
+ }
+
+ public class ClearImagesShould
+ {
+ [Theory, ScanningDeviceAutoData]
+ public void EmptyImagesCollection([Frozen] IList images, ScanningDevice sut)
+ {
+ sut.Images = images;
+ sut.Images
+ .Should().NotBeEmpty()
+ .And.BeEquivalentTo(images);
+
+ sut.ClearImages();
+
+ sut.Images
+ .Should().NotBeNull()
+ .And.BeEmpty();
+ }
+ }
+
+ public class GetImageShould
+ {
+ [Theory, ScanningDeviceAutoData]
+ public void ReturnImagesForSupportedFormat([Frozen] IEnumerable images, [Greedy] ScanningDevice sut)
+ {
+ sut.GetImage(ScannerSettingsCustomization.SupportedImageFormat);
+
+ sut.Images.Count.Should().Be(images.Count());
+
+ foreach (var (image, deviceImage) in Enumerable.Zip(images, sut.Images))
+ {
+ // Image data won't match exactly because headers will differ due to conversion
+ image.Should().IntersectWith(deviceImage);
+ }
+ }
+
+ [Theory, ScanningDeviceAutoData]
+ public void ThrowIfImageFormatNotSupported([Greedy] ScanningDevice sut)
+ {
+ sut.Invoking(x => x.GetImage(ScanningDevice.ImageFormat.Gif))
+ .Should().Throw();
+ sut.Images.Should().BeEmpty();
+ }
+
+ [Theory, ScanningDeviceAutoData]
+ public void ThrowCOMExceptionIfDeviceNotConnected([Greedy] ScanningDevice sut)
+ {
+ sut.IsConnected = false;
+
+ sut.Invoking(x => x.GetImage(ScannerSettingsCustomization.SupportedImageFormat))
+ .Should().Throw();
+ sut.Images.Should().BeEmpty();
+ }
+
+ [Theory, ScanningDeviceAutoData]
+ public void RaiseIsImagingPropertyChanged([Greedy] ScanningDevice sut)
+ {
+ // IsImaging should only be true during the operation of GetImage()
+ sut.IsImaging.Should().BeFalse();
+
+ using (var monitoredSubject = sut.Monitor())
+ {
+ monitoredSubject.Subject.GetImage(ScannerSettingsCustomization.SupportedImageFormat);
+
+ monitoredSubject.Should().RaisePropertyChangeFor(x => x.IsImaging);
+ }
+
+ sut.IsImaging.Should().BeFalse();
+ }
+ }
+
+ public class SaveImageShould
+ {
+ private readonly string filename = "Filename";
+ private readonly string directory = new MockFileSystem().Path.Combine("C:", "Directory");
+
+ [Theory, ScanningDeviceAutoData]
+ public void ThrowInvalidOperationExceptionIfNoImages([Greedy] ScanningDevice sut)
+ {
+ sut.Images = null;
+
+ sut
+ .Invoking(i => i.SaveImage(filename, directory, ScannerSettingsCustomization.SupportedImageFormat))
+ .Should().Throw();
+ }
+
+ [Theory, ScanningDeviceAutoData]
+ public void ThrowArgumentExceptionIfNoFilePath([CollectionSize(1)] IList images, [Greedy] ScanningDevice sut)
+ {
+ sut.Images = images;
+
+ sut
+ .Invoking(i => i.SaveImage(string.Empty, string.Empty, ScannerSettingsCustomization.SupportedImageFormat))
+ .Should().Throw()
+ .WithMessage("A valid filename and directory must be supplied");
+ }
+
+ [Theory, ScanningDeviceAutoData]
+ public void SaveImageWithFilePath([CollectionSize(1)] IList images, [Frozen] IFileSystem fileSystem, [Greedy] ScanningDevice sut)
+ {
+ sut.Images = images;
+
+ var result = sut.SaveImage(filename, directory, ScannerSettingsCustomization.SupportedImageFormat);
+
+ result.Should().HaveCount(images.Count);
+ (fileSystem as MockFileSystem).AllFiles.Should().BeEquivalentTo(result);
+ foreach (var filePath in result)
+ {
+ var expectedPath = fileSystem.Path.Combine(directory, filename);
+ expectedPath = fileSystem.Path.ChangeExtension(expectedPath, ScannerSettingsCustomization.SupportedImageFormat.ToString());
+ filePath.Equals(expectedPath, StringComparison.OrdinalIgnoreCase).Should().BeTrue();
+ }
+ }
+
+ [Theory, ScanningDeviceAutoData]
+ public void AppendCountToFilePaths([CollectionSize(2)] IList images, [Frozen] IFileSystem fileSystem, [Greedy] ScanningDevice sut)
+ {
+ var count = 0;
+ sut.Images = images;
+
+ var result = sut.SaveImage(filename, directory, ScannerSettingsCustomization.SupportedImageFormat);
+
+ result.Should().HaveCount(images.Count);
+ (fileSystem as MockFileSystem).AllFiles.Should().BeEquivalentTo(result);
+ foreach (var filePath in result)
+ {
+ var expectedPath = fileSystem.Path.Combine(directory, $"{filename}_{++count}");
+ expectedPath = fileSystem.Path.ChangeExtension(expectedPath, ScannerSettingsCustomization.SupportedImageFormat.ToString());
+ filePath.Equals(expectedPath, StringComparison.OrdinalIgnoreCase).Should().BeTrue();
+ }
+ }
+
+ [Theory, ScanningDeviceAutoData]
+ public void WriteImageDataToDisk([CollectionSize(2)] IList images, [Frozen] IFileSystem fileSystem, [Greedy] ScanningDevice sut)
+ {
+ var fs = fileSystem as MockFileSystem;
+ sut.Images = images;
+
+ var result = sut.SaveImage(filename, directory, ScannerSettingsCustomization.SupportedImageFormat);
+
+ result.Should().HaveCount(images.Count);
+ fs.AllFiles.Should().BeEquivalentTo(result);
+ foreach (var (filePath, image) in Enumerable.Zip(fs.AllFiles, images))
+ {
+ // Image data won't match exactly because headers will differ due to conversion
+ fs.GetFile(filePath).Contents.Should().IntersectWith(image);
+ }
+ }
+ }
+
+ public class EqualsShould
+ {
+ [Theory, ScanningDeviceAutoData]
+ public void BeEquivalentToClone([Frozen] ScannerSettings _, ScanningDevice sut, ScanningDevice clone)
+ {
+ sut.Equals(clone).Should().BeTrue();
+ }
+
+ [Theory, ScanningDeviceAutoData]
+ public void EqualSettingsHash([Frozen] ScannerSettings settings, ScanningDevice sut)
+ {
+ var result = sut.GetHashCode();
+
+ result.Should().Be(settings.Id.GetHashCode());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/MoseyTests/Services/Imaging/ScanningDevicesTests.cs b/MoseyTests/Services/Imaging/ScanningDevicesTests.cs
new file mode 100644
index 0000000..2df1e13
--- /dev/null
+++ b/MoseyTests/Services/Imaging/ScanningDevicesTests.cs
@@ -0,0 +1,233 @@
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using NUnit.Framework;
+using AutoFixture.NUnit3;
+using Moq;
+using FluentAssertions;
+using DNTScanner.Core;
+using Mosey.Models;
+using MoseyTests.Extensions;
+using MoseyTests.AutoData;
+
+namespace Mosey.Services.Imaging.Tests
+{
+ public class ScanningDevicesTests
+ {
+ public class ConstructorShould
+ {
+ [Theory, ScanningDeviceAutoData]
+ public void InitializeCollection(ScanningDevices sut)
+ {
+ sut.AssertAllPropertiesAreNotDefault();
+ sut.IsEmpty.Should().BeFalse();
+ sut.Devices.Count().Should().Be(3);
+ sut.Devices.All(device => device.IsEnabled == true).Should().BeTrue();
+ }
+
+ [Theory, ScanningDeviceAutoData]
+ public void InitializeCollectionGreedy([Greedy] ScanningDevices sut)
+ {
+ sut.AssertAllPropertiesAreNotDefault();
+ sut.IsEmpty.Should().BeFalse();
+ sut.Devices.Count().Should().Be(3);
+ sut.Devices.All(device => device.IsEnabled == true).Should().BeTrue();
+ }
+ }
+
+ public class AddDeviceShould
+ {
+ [Theory, ScanningDeviceAutoData]
+ public void ThrowIfDeviceIdExists([Frozen, CollectionSize(5)] IEnumerable scannerSettings, ScanningDevices sut)
+ {
+ var existingId = scannerSettings.First().Id;
+
+ sut.Invoking(x => x.AddDevice(deviceID: existingId))
+ .Should().Throw();
+ sut.Devices.Should()
+ .HaveCount(5)
+ .And.OnlyHaveUniqueItems()
+ .And.Contain(i => i.DeviceID == existingId);
+ }
+
+ [Theory, ScanningDeviceAutoData]
+ public void ThrowIfDeviceInstanceExists(
+ [Frozen] IImagingDeviceConfig deviceConfig,
+ [Frozen] ScannerSettings settings,
+ [Frozen, CollectionSize(1)] IEnumerable scannerSettings,
+ ScanningDevices sut)
+ {
+ var existingInstance = new ScanningDevice(settings, deviceConfig);
+
+ sut.Invoking(x => x.AddDevice(existingInstance))
+ .Should().Throw();
+ sut.Devices.Should()
+ .HaveCount(1)
+ .And.OnlyHaveUniqueItems()
+ .And.Contain(i => i.DeviceID == existingInstance.DeviceID);
+ }
+
+ [Theory, ScanningDeviceAutoData]
+ public void ContainUniqueDevices(
+ ScanningDevice device,
+ [Frozen] IImagingDeviceConfig deviceConfig,
+ [Frozen, CollectionSize(5)] IEnumerable scannerSettings,
+ ScanningDevices sut)
+ {
+ var scanningDevices = scannerSettings.Select(x => new ScanningDevice(x, deviceConfig));
+
+ sut.AddDevice(device);
+
+ sut.Devices.Should()
+ .HaveCount(6)
+ .And.OnlyHaveUniqueItems()
+ .And.Contain(scanningDevices)
+ .And.Contain(i => i.DeviceID == device.DeviceID);
+ }
+ }
+
+ public class DisableAllShould
+ {
+ [Theory, ScanningDeviceAutoData]
+ public void SetAllDevicesToDisabled(ScanningDevices sut)
+ {
+ sut.Devices.All(device => device.IsEnabled).Should().BeTrue();
+
+ sut.DisableAll();
+
+ sut.Devices.All(device => !device.IsEnabled).Should().BeTrue();
+ }
+ }
+
+ public class EnableAllShould
+ {
+ [Theory, ScanningDeviceAutoData]
+ public void SetAllDevicesToEnabled(ScanningDevices sut)
+ {
+ foreach (var device in sut.Devices)
+ {
+ device.IsEnabled = false;
+ }
+ sut.Devices.All(device => !device.IsEnabled).Should().BeTrue();
+
+ sut.EnableAll();
+
+ sut.Devices.All(device => device.IsEnabled).Should().BeTrue();
+ }
+ }
+
+ public class RefreshDevicesShould
+ {
+ [Theory, ScanningDeviceAutoData]
+ public void DisconnectAllDevicesIfNotFound(ScanningDevices sut)
+ {
+ sut.RefreshDevices();
+
+ sut.Devices.All(device => !device.IsConnected).Should().BeTrue();
+ }
+
+ [Theory, ScanningDeviceAutoData]
+ public void DisconnectSingleDeviceIfNotFound(
+ [Frozen] Mock systemDevices,
+ [Frozen] IEnumerable scannerSettings)
+ {
+ systemDevices
+ .Setup(mock => mock.ScannerSettings(It.IsAny()))
+ .Returns(scannerSettings);
+ var sut = new ScanningDevices(systemDevices.Object);
+ var initalDevices = sut.Devices;
+
+ // Add the existing DeviceIds except one so the device will appear disconnected
+ var scannerProperties = new List>();
+ foreach (var deviceID in sut.Devices.Skip(1).Select(d => d.DeviceID))
+ {
+ scannerProperties.Add(new Dictionary() { { "Unique Device ID", deviceID } });
+ }
+ systemDevices
+ .Setup(mock => mock.ScannerProperties(It.IsAny()))
+ .Returns(scannerProperties);
+
+ sut.RefreshDevices();
+
+ sut.Devices
+ .Should().HaveCount(3)
+ .And.BeEquivalentTo(initalDevices);
+ sut.Devices
+ .Skip(1).All(device => device.IsConnected)
+ .Should().BeTrue();
+ sut.Devices
+ .First().IsConnected
+ .Should().BeFalse();
+ }
+
+ [Theory, ScanningDeviceAutoData]
+ public void AddNewDevicesToCollection(
+ [Frozen] Mock systemDevices,
+ [Frozen, CollectionSize(4)] IEnumerable scannerSettings,
+ ScannerSettings newScannerSettings)
+ {
+ systemDevices
+ .Setup(mock => mock.ScannerSettings(It.IsAny()))
+ .Returns(scannerSettings);
+ var sut = new ScanningDevices(systemDevices.Object);
+ var initalDevices = sut.Devices;
+
+ // Add an extra scanner to the collection
+ scannerSettings = scannerSettings.Append(newScannerSettings);
+ var scannerProperties = new List>();
+ foreach (var deviceID in sut.Devices.Select(d => d.DeviceID).Append(newScannerSettings.Id))
+ {
+ scannerProperties.Add(new Dictionary() { { "Unique Device ID", deviceID } });
+ }
+ systemDevices
+ .Setup(mock => mock.ScannerProperties(It.IsAny()))
+ .Returns(scannerProperties);
+ systemDevices
+ .Setup(mock => mock.ScannerSettings(It.IsAny()))
+ .Returns(scannerSettings);
+
+ sut.RefreshDevices();
+
+ sut.Devices
+ .Should().HaveCount(5)
+ .And.Contain(initalDevices);
+ sut.Devices
+ .Any(d => d.DeviceID == newScannerSettings.Id)
+ .Should().BeTrue();
+ }
+ }
+
+ public class SetDeviceEnabledShould
+ {
+ [Theory, ScanningDeviceAutoData]
+ public void EnableCorrectDevice(ScanningDevices sut)
+ {
+ var deviceId = sut.Devices.First().DeviceID;
+ foreach (var device in sut.Devices)
+ {
+ device.IsEnabled = false;
+ }
+
+ sut.SetDeviceEnabled(deviceId, true);
+
+ sut.Devices.First().IsEnabled.Should().BeTrue();
+ sut.Devices.Skip(1).All(d => !d.IsEnabled).Should().BeTrue();
+ }
+
+ [Theory, ScanningDeviceAutoData]
+ public void DisableCorrectDevice(ScanningDevices sut)
+ {
+ var deviceId = sut.Devices.First().DeviceID;
+ foreach (var device in sut.Devices)
+ {
+ device.IsEnabled = true;
+ }
+
+ sut.SetDeviceEnabled(deviceId, true);
+
+ sut.Devices.First().IsEnabled.Should().BeTrue();
+ sut.Devices.Skip(1).All(d => d.IsEnabled).Should().BeTrue();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/MoseyTests/ViewModels/MainViewModelTests.cs b/MoseyTests/ViewModels/MainViewModelTests.cs
new file mode 100644
index 0000000..727daa7
--- /dev/null
+++ b/MoseyTests/ViewModels/MainViewModelTests.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using AutoFixture.NUnit3;
+using NSubstitute;
+using FluentAssertions;
+using Mosey.Models;
+using MoseyTests.AutoData;
+using MoseyTests.Extensions;
+
+namespace Mosey.ViewModels.Tests
+{
+ public class MainViewModelTests
+ {
+ public class ConstructorShould
+ {
+ [Theory, MainViewModelAutoData]
+ public void InitializeProperties(MainViewModel sut)
+ {
+ sut.AssertAllPropertiesAreNotDefault();
+ }
+
+ [Theory, MainViewModelAutoData]
+ public void InitializeScanningDevicesCollection([Frozen] IEnumerable imagingDevices, MainViewModel sut)
+ {
+ sut
+ .ScanningDevices.Devices.Select(d => d.DeviceID)
+ .Should().BeEquivalentTo(imagingDevices.Select(d => d.DeviceID));
+ }
+ }
+
+ public class StartScanShould
+ {
+ [Theory, MainViewModelAutoData]
+ public void SetScanningProperties(MainViewModel sut)
+ {
+ sut.StartScan();
+
+ sut.IsScanRunning.Should().BeTrue();
+ sut.ScanRepetitionsCount.Should().Be(0);
+ sut.ScanNextTime.Should().Be(TimeSpan.Zero);
+ }
+
+ [Theory, MainViewModelAutoData]
+ public void RaisePropertyChanged(MainViewModel sut)
+ {
+ using (var monitoredSubject = sut.Monitor())
+ {
+ monitoredSubject.Subject.StartScan();
+
+ monitoredSubject.Should().RaisePropertyChangeFor(x => x.IsScanRunning);
+ monitoredSubject.Should().RaisePropertyChangeFor(x => x.ScanFinishTime);
+ monitoredSubject.Should().RaisePropertyChangeFor(x => x.StartStopScanCommand);
+ }
+ }
+ }
+
+ public class RefreshDevicesAsyncShould
+ {
+ [Theory, MainViewModelAutoData]
+ public async Task RepeatRefreshDevices([Frozen] IImagingDevices imagingDevices, MainViewModel sut)
+ {
+ using var cts = new CancellationTokenSource();
+ cts.CancelAfter(2000);
+
+ await sut.RefreshDevicesAsync(0, cts.Token);
+
+ imagingDevices
+ .ReceivedWithAnyArgs().RefreshDevices(null, true);
+ imagingDevices
+ .ReceivedCalls().Where(x => x.GetMethodInfo().Name == nameof(imagingDevices.RefreshDevices))
+ .Count().Should().BeGreaterThan(1);
+ }
+
+ [Theory, MainViewModelAutoData]
+ public async Task CancelTask([Frozen] IImagingDevices imagingDevices, MainViewModel sut)
+ {
+ using var cts = new CancellationTokenSource();
+ cts.CancelAfter(0);
+
+ await sut.RefreshDevicesAsync(0, cts.Token);
+
+ imagingDevices
+ .DidNotReceiveWithAnyArgs().RefreshDevices(null, true);
+ }
+
+ [Theory, MainViewModelAutoData]
+ public async Task RaisePropertyChanged(MainViewModel sut)
+ {
+ using var cts = new CancellationTokenSource();
+ cts.CancelAfter(1000);
+
+ using (var monitoredSubject = sut.Monitor())
+ {
+ await sut.RefreshDevicesAsync(0, cts.Token);
+
+ monitoredSubject.Should().RaisePropertyChangeFor(x => x.ScanningDevices);
+ monitoredSubject.Should().RaisePropertyChangeFor(x => x.StartScanCommand);
+ monitoredSubject.Should().RaisePropertyChangeFor(x => x.StartStopScanCommand);
+ }
+ }
+ }
+ }
+}