using System.Collections.Concurrent; using System.Reflection; using VisionBuilder.UI.Common; using VisionBuilder.UI.Common.Commands; using VisionBuilder.UI.Common.Processing; using VisionBuilder.UI.Common.ViewModel.Classes; using VisionBuilder.UI.Statistics; namespace VisionBuilder.UI.Tests; [TestClass] public class VisionBuilderStatisticsTests { private string _tempDirectory; private VisionBuilderStatisticsSettings _settings; private TestRecognitionControl _recognitionControl; private TestLoadingService _loadingService; [TestInitialize] public void Setup() { _tempDirectory = Path.Combine(Path.GetTempPath(), "VisionBuilderStatisticsTests", Guid.NewGuid().ToString()); Directory.CreateDirectory(_tempDirectory); _settings = new VisionBuilderStatisticsSettings("TestCamera") { PathTemplate = _tempDirectory, StartDateColumnName = "Start Date", StartTimeColumnName = "Start time", EndDateColumnName = "End Date", EndTimeColumnName = "End time", ErrorTimeColumnName = "Error time", ProductColumnName = "Product", ErrorColumnName = "Error" }; _recognitionControl = new TestRecognitionControl(); _loadingService = new TestLoadingService(); } [TestCleanup] public void Cleanup() { if (Directory.Exists(_tempDirectory)) { Directory.Delete(_tempDirectory, true); } } [TestMethod] public void SessionStarted_CreatesTemporaryFile_WithCorrectHeaders() { // Arrange var statistics = new VisionBuilderStatistics(_settings, _recognitionControl, _loadingService); statistics.InitializeModule(); var sessionStartEvent = new SessionStartedEvent { RecipeName = "TestRecipe", SessionStarted = DateTime.Now }; // Act _recognitionControl.TriggerSessionStarted(sessionStartEvent); statistics.Dispose(); // Ensure file is written and closed // Assert var tmpFiles = Directory.GetFiles(_tempDirectory, "tmp_*.csv"); Assert.AreEqual(1, tmpFiles.Length, "Should create exactly one temporary file"); var lines = File.ReadAllLines(tmpFiles[0]); Assert.IsTrue(lines.Length >= 2, "Should have header and at least one data line"); var expectedHeaders = new[] { "Start Date", "Start time", "End Date", "End time", "Error time", "Product", "Error" }; var actualHeaders = lines[0].Split(','); CollectionAssert.AreEqual(expectedHeaders, actualHeaders, "Headers should match settings"); var firstDataLine = lines[1].Split(','); Assert.IsTrue(DateTime.TryParse(firstDataLine[0], out _), "First column should be a valid date"); Assert.IsTrue(DateTime.TryParse(firstDataLine[1], out _), "Second column should be a valid time"); } [TestMethod] public void ImageProcessed_WithError_RecordsDefectInFile() { var statistics = new VisionBuilderStatistics(_settings, _recognitionControl, _loadingService); statistics.InitializeModule(); // Arrange var sessionStartEvent = new SessionStartedEvent { RecipeName = "TestRecipe", SessionStarted = DateTime.Now }; var imageProcessedEvent = new ImageProcessedEvent { HasError = true, ErrorNames = new List { "TestDefect" }, RecipeName = "TestRecipe", SessionStart = DateTime.Now }; // Act _recognitionControl.TriggerSessionStarted(sessionStartEvent); _recognitionControl.TriggerImageProcessed(imageProcessedEvent); statistics.Dispose(); // Ensure file is written and closed // Assert var tmpFiles = Directory.GetFiles(_tempDirectory, "tmp_*.csv"); var lines = File.ReadAllLines(tmpFiles[0]); Assert.IsTrue(lines.Length >= 3, "Should have header, session start, and error record"); var errorLine = lines[2].Split(','); Assert.AreEqual("TestDefect", errorLine[6], "Error name should be recorded in correct column"); Assert.AreEqual("TestRecipe", errorLine[5], "Recipe name should be recorded in correct column"); Assert.IsTrue(DateTime.TryParse(errorLine[4], out _), "Error time should be valid"); // Verify defect counting using reflection var defectsField = typeof(VisionBuilderStatistics).GetField("_defects", BindingFlags.NonPublic | BindingFlags.Instance); var defects = (ConcurrentDictionary)defectsField.GetValue(statistics); Assert.AreEqual(1, defects["TestDefect"], "Defect count should be incremented"); } [TestMethod] public void ImageProcessed_WithoutError_DoesNotRecordDefect() { var statistics = new VisionBuilderStatistics(_settings, _recognitionControl, _loadingService); statistics.InitializeModule(); // Arrange var sessionStartEvent = new SessionStartedEvent { RecipeName = "TestRecipe", SessionStarted = DateTime.Now }; var imageProcessedEvent = new ImageProcessedEvent { HasError = false, ErrorNames = new List(), RecipeName = "TestRecipe", SessionStart = DateTime.Now }; // Act _recognitionControl.TriggerSessionStarted(sessionStartEvent); _recognitionControl.TriggerImageProcessed(imageProcessedEvent); statistics.Dispose(); // Ensure file is written and closed // Assert var tmpFiles = Directory.GetFiles(_tempDirectory, "tmp_*.csv"); var lines = File.ReadAllLines(tmpFiles[0]); Assert.AreEqual(2, lines.Length, "Should only have header and session start line"); // Verify defect counting using reflection var defectsField = typeof(VisionBuilderStatistics).GetField("_defects", BindingFlags.NonPublic | BindingFlags.Instance); var defects = (ConcurrentDictionary)defectsField.GetValue(statistics); Assert.AreEqual(0, defects.Count, "No defects should be recorded"); } [TestMethod] public void SessionEnded_WritesEndTimeAndDisposesCsv() { var statistics = new VisionBuilderStatistics(_settings, _recognitionControl, _loadingService) { DoNotDeleteTempFiles = true }; statistics.InitializeModule(); // Arrange var sessionStartEvent = new SessionStartedEvent { RecipeName = "TestRecipe", SessionStarted = DateTime.Now }; var sessionEndEvent = new SessionEndedEvent { SessionEnded = DateTime.Now }; // Act _recognitionControl.TriggerSessionStarted(sessionStartEvent); _recognitionControl.TriggerSessionEnded(sessionEndEvent); statistics.Dispose(); // Ensure file is written and closed // Assert var tmpFiles = Directory.GetFiles(_tempDirectory, "tmp_*.csv"); var lines = File.ReadAllLines(tmpFiles[0]); Assert.IsTrue(lines.Length >= 3, "Should have header, session start, and session end"); var endLine = lines[2].Split(','); Assert.IsTrue(DateTime.TryParse(endLine[2], out _), "End date should be valid"); Assert.IsTrue(DateTime.TryParse(endLine[3], out _), "End time should be valid"); Assert.IsTrue(_loadingService.StartLoadingCalled, "Should start loading during session end"); Assert.IsTrue(_loadingService.StopLoadingCalled, "Should stop loading after session end"); } [TestMethod] public void WriteFinalStatistics_ConsolidatesTemporaryFiles() { var statistics = new VisionBuilderStatistics(_settings, _recognitionControl, _loadingService); statistics.InitializeModule(); // Arrange var sessionStartEvent = new SessionStartedEvent { RecipeName = "TestRecipe", SessionStarted = DateTime.Now }; var imageProcessedEvent = new ImageProcessedEvent { HasError = true, ErrorNames = new List { "TestDefect" }, RecipeName = "TestRecipe", SessionStart = DateTime.Now }; var sessionEndEvent = new SessionEndedEvent { SessionEnded = DateTime.Now }; // Act _recognitionControl.TriggerSessionStarted(sessionStartEvent); _recognitionControl.TriggerImageProcessed(imageProcessedEvent); _recognitionControl.TriggerSessionEnded(sessionEndEvent); statistics.Dispose(); // Ensure file is written and closed // Assert var tmpFiles = Directory.GetFiles(_tempDirectory, "tmp_*.csv"); Assert.AreEqual(0, tmpFiles.Length, "Temporary files should be deleted after consolidation"); var finalFile = Path.Combine(_tempDirectory, "statistics_by_events.csv"); Assert.IsTrue(File.Exists(finalFile), "Final statistics file should be created"); var finalLines = File.ReadAllLines(finalFile); Assert.IsTrue(finalLines.Length >= 2, "Final file should contain session data"); // Verify the defect was recorded in the final file var defectLine = finalLines.FirstOrDefault(l => l.Contains("TestDefect")); Assert.IsNotNull(defectLine, "Defect should be recorded in final file"); } [TestMethod] public void MultipleDefects_CountedCorrectly() { var statistics = new VisionBuilderStatistics(_settings, _recognitionControl, _loadingService); statistics.InitializeModule(); // Arrange var sessionStartEvent = new SessionStartedEvent { RecipeName = "TestRecipe", SessionStarted = DateTime.Now }; // Act _recognitionControl.TriggerSessionStarted(sessionStartEvent); // Process multiple images with same defect for (int i = 0; i < 3; i++) { _recognitionControl.TriggerImageProcessed(new ImageProcessedEvent { HasError = true, ErrorNames = new List { "DefectA" }, RecipeName = "TestRecipe", SessionStart = DateTime.Now }); } // Process images with different defect for (int i = 0; i < 2; i++) { _recognitionControl.TriggerImageProcessed(new ImageProcessedEvent { HasError = true, ErrorNames = new List { "DefectB" }, RecipeName = "TestRecipe", SessionStart = DateTime.Now }); } statistics.Dispose(); // Ensure file is written and closed // Assert var defectsField = typeof(VisionBuilderStatistics).GetField("_defects", BindingFlags.NonPublic | BindingFlags.Instance); var defects = (ConcurrentDictionary)defectsField.GetValue(statistics); Assert.AreEqual(3, defects["DefectA"], "DefectA should be counted correctly"); Assert.AreEqual(2, defects["DefectB"], "DefectB should be counted correctly"); } [TestMethod] public void PathTemplate_WithVariables_ReplacedCorrectly() { // Arrange var customSettings = new VisionBuilderStatisticsSettings("TestCamera") { PathTemplate = Path.Combine(_tempDirectory, "$RECIPE_NAME", "$SS_YEAR", "$SS_MONTH") }; var customStatistics = new VisionBuilderStatistics(customSettings, _recognitionControl, _loadingService); customStatistics.InitializeModule(); var testDate = new DateTime(2023, 5, 15, 10, 30, 0); var sessionStartEvent = new SessionStartedEvent { RecipeName = "MyRecipe", SessionStarted = new DateTime(2023, 5, 15, 10, 30, 0) }; // Act _recognitionControl.TriggerSessionStarted(sessionStartEvent); customStatistics.Dispose(); // Ensure file is written and closed // Assert var expectedPath = Path.Combine(_tempDirectory, "MyRecipe", testDate.ToString("yyyy"), testDate.ToString("MMMM")); Assert.IsTrue(Directory.Exists(expectedPath), "Directory should be created with expanded variables"); var tmpFiles = Directory.GetFiles(expectedPath, "tmp_*.csv"); Assert.AreEqual(1, tmpFiles.Length, "Temporary file should be created in correct directory"); } [TestMethod] public void CustomColumnNames_UsedInHeaders() { // Arrange var customSettings = new VisionBuilderStatisticsSettings("TestCamera") { PathTemplate = _tempDirectory, StartDateColumnName = "Custom Start Date", StartTimeColumnName = "Custom Start Time", EndDateColumnName = "Custom End Date", EndTimeColumnName = "Custom End Time", ErrorTimeColumnName = "Custom Error Time", ProductColumnName = "Custom Product", ErrorColumnName = "Custom Error" }; var customStatistics = new VisionBuilderStatistics(customSettings, _recognitionControl, _loadingService); customStatistics.InitializeModule(); var sessionStartEvent = new SessionStartedEvent { RecipeName = "TestRecipe", SessionStarted = DateTime.Now }; // Act _recognitionControl.TriggerSessionStarted(sessionStartEvent); customStatistics.Dispose(); // Assert var tmpFiles = Directory.GetFiles(_tempDirectory, "tmp_*.csv"); var lines = File.ReadAllLines(tmpFiles[0]); var expectedHeaders = new[] { "Custom Start Date", "Custom Start Time", "Custom End Date", "Custom End Time", "Custom Error Time", "Custom Product", "Custom Error" }; var actualHeaders = lines[0].Split(','); CollectionAssert.AreEqual(expectedHeaders, actualHeaders, "Custom column names should be used in headers"); } [TestMethod] public void WriteMonthStatistics_CreatesMonthlyFileWithDefectCounts() { var statistics = new VisionBuilderStatistics(_settings, _recognitionControl, _loadingService); statistics.InitializeModule(); // Arrange var sessionStartEvent = new SessionStartedEvent { RecipeName = "TestRecipe", SessionStarted = new DateTime(2023, 5, 15, 10, 30, 0) }; var imageProcessedEvent1 = new ImageProcessedEvent { HasError = true, ErrorNames = new List { "DefectA" }, RecipeName = "TestRecipe", SessionStart = new DateTime(2023, 5, 15, 10, 30, 0) }; var imageProcessedEvent2 = new ImageProcessedEvent { HasError = true, ErrorNames = new List { "DefectB" }, RecipeName = "TestRecipe", SessionStart = new DateTime(2023, 5, 15, 10, 30, 0) }; var sessionEndEvent = new SessionEndedEvent { SessionEnded = new DateTime(2023, 5, 15, 11, 45, 0) }; // Act _recognitionControl.TriggerSessionStarted(sessionStartEvent); _recognitionControl.TriggerImageProcessed(imageProcessedEvent1); _recognitionControl.TriggerImageProcessed(imageProcessedEvent1); // Same defect again _recognitionControl.TriggerImageProcessed(imageProcessedEvent2); _recognitionControl.TriggerSessionEnded(sessionEndEvent); statistics.Dispose(); // Assert var monthlyFile = Path.Combine(_tempDirectory, "statistics_month.csv"); Assert.IsTrue(File.Exists(monthlyFile), "Monthly statistics file should be created"); var monthlyLines = File.ReadAllLines(monthlyFile); Assert.IsTrue(monthlyLines.Length >= 3, "Should have header and defect records"); var expectedHeaders = new[] { "Date start", "Time start", "Date end", "Time end", "Product", "Error type", "Error count" }; var actualHeaders = monthlyLines[0].Split(','); CollectionAssert.AreEqual(expectedHeaders, actualHeaders, "Monthly file should have correct headers"); // Verify defect counts var defectALine = monthlyLines.FirstOrDefault(l => l.Contains("DefectA")); var defectBLine = monthlyLines.FirstOrDefault(l => l.Contains("DefectB")); Assert.IsNotNull(defectALine, "DefectA should be recorded in monthly file"); Assert.IsNotNull(defectBLine, "DefectB should be recorded in monthly file"); var defectAData = defectALine.Split(','); var defectBData = defectBLine.Split(','); Assert.AreEqual("2", defectAData[6], "DefectA count should be 2"); Assert.AreEqual("1", defectBData[6], "DefectB count should be 1"); Assert.AreEqual("TestRecipe", defectAData[4], "Product name should be correct"); Assert.AreEqual("TestRecipe", defectBData[4], "Product name should be correct"); // Verify dates and times Assert.IsTrue(DateTime.TryParse(defectAData[0], out var startDate), "Start date should be valid"); Assert.IsTrue(DateTime.TryParse(defectAData[1], out var startTime), "Start time should be valid"); Assert.IsTrue(DateTime.TryParse(defectAData[2], out var endDate), "End date should be valid"); Assert.IsTrue(DateTime.TryParse(defectAData[3], out var endTime), "End time should be valid"); } } // Test implementation of IRecognitionControl using reflection approach public class TestRecognitionControl : IRecognitionControl { public void Resume() { } public event Action ImageProcessed = delegate { }; public event Action SessionStarted = delegate { }; public event Action SessionEnded = delegate { }; public List GetRecipesData() => new List(); public void SetRecipe(RecipeData recipe) { } public void Start() { } public void Stop() { } public void Pause() { } public void TriggerImageProcessed(ImageProcessedEvent eventArgs) => ImageProcessed(eventArgs); public void TriggerSessionStarted(SessionStartedEvent eventArgs) => SessionStarted(eventArgs); public void TriggerSessionEnded(SessionEndedEvent eventArgs) => SessionEnded(eventArgs); } // Test implementation of ILoadingService public class TestLoadingService : ILoadingService { public bool StartLoadingCalled { get; private set; } public bool StopLoadingCalled { get; private set; } public string LastLoadingMessage { get; private set; } public void StartLoading(string message) { StartLoadingCalled = true; LastLoadingMessage = message; } public void StopLoading(string message) { StopLoadingCalled = true; LastLoadingMessage = message; } }