diff --git a/src/LogExpert.Core/Classes/Persister/Persister.cs b/src/LogExpert.Core/Classes/Persister/Persister.cs index 25011252..ab2247ee 100644 --- a/src/LogExpert.Core/Classes/Persister/Persister.cs +++ b/src/LogExpert.Core/Classes/Persister/Persister.cs @@ -9,6 +9,7 @@ namespace LogExpert.Core.Classes.Persister; +//Todo Move Persister to its own assembly LogExpert.Persister public static class Persister { #region Fields @@ -46,12 +47,13 @@ public static class Persister /// The user preferences that determine the save location and other settings. This parameter cannot be . /// The full path of the file where the persistence data was saved. - public static string SavePersistenceData (string logFileName, PersistenceData persistenceData, Preferences preferences) + public static string SavePersistenceData (string logFileName, PersistenceData persistenceData, Preferences preferences, string applicationStartupPath) { ArgumentNullException.ThrowIfNull(preferences); ArgumentNullException.ThrowIfNull(persistenceData); + ArgumentException.ThrowIfNullOrWhiteSpace(applicationStartupPath); - var fileName = persistenceData.SessionFileName ?? BuildPersisterFileName(logFileName, preferences); + var fileName = persistenceData.SessionFileName ?? BuildPersisterFileName(logFileName, preferences, applicationStartupPath); if (preferences.SaveLocation == SessionSaveLocation.SameDir) { @@ -82,10 +84,12 @@ public static string SavePersistenceDataWithFixedName (string persistenceFileNam /// The name of the log file to load persistence data from. This value cannot be null. /// The preferences used to determine the file path and loading behaviour. This value cannot be null. /// The loaded object containing the persistence information. - public static PersistenceData LoadPersistenceData (string logFileName, Preferences preferences) + public static PersistenceData LoadPersistenceData (string logFileName, Preferences preferences, string applicationStartupPath) { ArgumentNullException.ThrowIfNull(preferences); - var fileName = BuildPersisterFileName(logFileName, preferences); + ArgumentNullException.ThrowIfNull(applicationStartupPath); + + var fileName = BuildPersisterFileName(logFileName, preferences, applicationStartupPath); return LoadInternal(fileName); } @@ -95,10 +99,12 @@ public static PersistenceData LoadPersistenceData (string logFileName, Preferenc /// The name of the log file used to determine the persistence data file. /// The preferences that influence the file name generation. Cannot be . /// A object containing the loaded data. - public static PersistenceData LoadPersistenceDataOptionsOnly (string logFileName, Preferences preferences) + public static PersistenceData LoadPersistenceDataOptionsOnly (string logFileName, Preferences preferences, string applicationStartupPath) { ArgumentNullException.ThrowIfNull(preferences); - var fileName = BuildPersisterFileName(logFileName, preferences); + ArgumentNullException.ThrowIfNull(applicationStartupPath); + + var fileName = BuildPersisterFileName(logFileName, preferences, applicationStartupPath); return LoadInternal(fileName); } @@ -149,7 +155,7 @@ public static PersistenceData Load (string fileName) /// The preferences that determine the save location and directory structure for the persister file. /// The full file path of the persister file, including the directory and file name, based on the specified log file /// name and preferences. - private static string BuildPersisterFileName (string logFileName, Preferences preferences) + private static string BuildPersisterFileName (string logFileName, Preferences preferences, string applicationStartupPath) { string dir; string file; @@ -180,8 +186,7 @@ private static string BuildPersisterFileName (string logFileName, Preferences pr } case SessionSaveLocation.ApplicationStartupDir: { - //TODO Add Application.StartupPath as Variable - dir = string.Empty;// Application.StartupPath + Path.DirectorySeparatorChar + "sessionfiles"; + dir = Path.Join(applicationStartupPath, "sessionFiles"); file = dir + Path.DirectorySeparatorChar + BuildSessionFileNameFromPath(logFileName); break; } @@ -213,12 +218,13 @@ PathTooLongException or /// underscores, and the file name is appended with the ".lxp" extension. private static string BuildSessionFileNameFromPath (string logFileName) { - var result = logFileName; - result = result.Replace(Path.DirectorySeparatorChar, '_'); - result = result.Replace(Path.AltDirectorySeparatorChar, '_'); - result = result.Replace(Path.VolumeSeparatorChar, '_'); - result += ".lxp"; - return result; + var result = new StringBuilder(); + _ = result.Append(logFileName); + _ = result.Replace(Path.DirectorySeparatorChar, '_'); + _ = result.Replace(Path.AltDirectorySeparatorChar, '_'); + _ = result.Replace(Path.VolumeSeparatorChar, '_'); + _ = result.Append(".lxp"); + return result.ToString(); } /// diff --git a/src/LogExpert.Persister.Tests/LogExpert.Persister.Tests.csproj b/src/LogExpert.Persister.Tests/LogExpert.Persister.Tests.csproj new file mode 100644 index 00000000..ae88c488 --- /dev/null +++ b/src/LogExpert.Persister.Tests/LogExpert.Persister.Tests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + true + LogExpert.Persister.Tests + LogExpert.Persister.Tests + + + + + + + + + + + + + + + + + + diff --git a/src/LogExpert.Persister.Tests/PersisterTests.cs b/src/LogExpert.Persister.Tests/PersisterTests.cs new file mode 100644 index 00000000..25300c8f --- /dev/null +++ b/src/LogExpert.Persister.Tests/PersisterTests.cs @@ -0,0 +1,656 @@ +using LogExpert.Core.Classes.Persister; +using LogExpert.Core.Config; + +namespace LogExpert.Persister.Tests; + +[TestFixture] +public class PersisterTests +{ + private string _testDirectory; + private string _applicationStartupPath; + private string _logFileName; + + [SetUp] + public void Setup () + { + // Create temporary test directory + _testDirectory = Path.Join(Path.GetTempPath(), "LogExpertTests", Guid.NewGuid().ToString()); + _ = Directory.CreateDirectory(_testDirectory); + + // Create a subdirectory to simulate application startup path + _applicationStartupPath = Path.Join(_testDirectory, "ApplicationPath"); + _ = Directory.CreateDirectory(_applicationStartupPath); + + // Create a test log file + _logFileName = Path.Join(_testDirectory, "test.log"); + File.WriteAllText(_logFileName, "Test log content"); + } + + [TearDown] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Unit Test")] + public void TearDown () + { + // Clean up test directory + if (Directory.Exists(_testDirectory)) + { + try + { + Directory.Delete(_testDirectory, true); + } + catch + { + // Ignore cleanup errors + } + } + } + + #region SavePersistenceData Tests - ApplicationStartupDir Location + + [Test] + public void SavePersistenceData_WithApplicationStartupDir_CreatesSessionFilesDirectory () + { + // Arrange + var preferences = new Preferences + { + SaveLocation = SessionSaveLocation.ApplicationStartupDir + }; + + var persistenceData = new PersistenceData + { + FileName = _logFileName + }; + + // Act + var savedFileName = Core.Classes.Persister.Persister.SavePersistenceData(_logFileName, persistenceData, preferences, _applicationStartupPath); + + // Assert + var expectedDirectory = Path.Join(_applicationStartupPath, "sessionFiles"); + Assert.That(Directory.Exists(expectedDirectory), Is.True, "sessionFiles directory should be created"); + Assert.That(savedFileName, Does.StartWith(expectedDirectory), "Saved file should be in sessionFiles directory"); + } + + [Test] + public void SavePersistenceData_WithApplicationStartupDir_SavesFileWithCorrectName () + { + // Arrange + var preferences = new Preferences + { + SaveLocation = SessionSaveLocation.ApplicationStartupDir + }; + + var persistenceData = new PersistenceData + { + FileName = _logFileName, + CurrentLine = 42, + FollowTail = true + }; + + // Act + var savedFileName = Core.Classes.Persister.Persister.SavePersistenceData(_logFileName, persistenceData, preferences, _applicationStartupPath); + + // Assert + Assert.That(File.Exists(savedFileName), Is.True, "Persistence file should exist"); + Assert.That(savedFileName, Does.EndWith(".lxp"), "Persistence file should have .lxp extension"); + } + + [Test] + public void SavePersistenceData_WithApplicationStartupDir_FileContainsCorrectData () + { + // Arrange + var preferences = new Preferences + { + SaveLocation = SessionSaveLocation.ApplicationStartupDir + }; + + var persistenceData = new PersistenceData + { + FileName = _logFileName, + CurrentLine = 42, + FollowTail = true, + FilterVisible = true + }; + + // Act + var savedFileName = Core.Classes.Persister.Persister.SavePersistenceData(_logFileName, persistenceData, preferences, _applicationStartupPath); + + // Assert + var savedContent = File.ReadAllText(savedFileName); + Assert.That(savedContent, Does.Contain("\"CurrentLine\": 42"), "Should contain CurrentLine value"); + Assert.That(savedContent, Does.Contain("\"FollowTail\": true"), "Should contain FollowTail value"); + Assert.That(savedContent, Does.Contain("\"FilterVisible\": true"), "Should contain FilterVisible value"); + } + + [Test] + public void SavePersistenceData_WithApplicationStartupDir_NullApplicationStartupPath_ThrowsArgumentNullException () + { + // Arrange + var preferences = new Preferences + { + SaveLocation = SessionSaveLocation.ApplicationStartupDir + }; + + var persistenceData = new PersistenceData + { + FileName = _logFileName + }; + + // Act & Assert + _ = Assert.Throws(() => + Core.Classes.Persister.Persister.SavePersistenceData(_logFileName, persistenceData, preferences, null)); + } + + #endregion + + #region SavePersistenceData Tests - Other Locations + + [Test] + public void SavePersistenceData_WithSameDir_DoesNotUseApplicationStartupPath () + { + // Arrange + var preferences = new Preferences + { + SaveLocation = SessionSaveLocation.SameDir + }; + + var persistenceData = new PersistenceData + { + FileName = _logFileName + }; + + // Act + var savedFileName = Core.Classes.Persister.Persister.SavePersistenceData(_logFileName, persistenceData, preferences, _applicationStartupPath); + + // Assert + Assert.That(savedFileName, Does.Not.Contain(_applicationStartupPath), "Should not use applicationStartupPath for SameDir location"); + Assert.That(savedFileName, Does.StartWith(_testDirectory), "Should save in same directory as log file"); + } + + [Test] + public void SavePersistenceData_WithDocumentsDir_DoesNotUseApplicationStartupPath () + { + // Arrange + var preferences = new Preferences + { + SaveLocation = SessionSaveLocation.DocumentsDir + }; + + var persistenceData = new PersistenceData + { + FileName = _logFileName + }; + + // Act + var savedFileName = Core.Classes.Persister.Persister.SavePersistenceData(_logFileName, persistenceData, preferences, _applicationStartupPath); + + // Assert + Assert.That(savedFileName, Does.Not.Contain(_applicationStartupPath), "Should not use applicationStartupPath for DocumentsDir location"); + var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + Assert.That(savedFileName, Does.StartWith(documentsPath), "Should save in Documents directory"); + } + + [Test] + public void SavePersistenceData_WithOwnDir_DoesNotUseApplicationStartupPath () + { + // Arrange + var customDirectory = Path.Join(_testDirectory, "CustomSessionDir"); + _ = Directory.CreateDirectory(customDirectory); + + var preferences = new Preferences + { + SaveLocation = SessionSaveLocation.OwnDir, + SessionSaveDirectory = customDirectory + }; + + var persistenceData = new PersistenceData + { + FileName = _logFileName + }; + + // Act + var savedFileName = Core.Classes.Persister.Persister.SavePersistenceData(_logFileName, persistenceData, preferences, _applicationStartupPath); + + // Assert + Assert.That(savedFileName, Does.Not.Contain(_applicationStartupPath), "Should not use applicationStartupPath for OwnDir location"); + Assert.That(savedFileName, Does.StartWith(customDirectory), "Should save in custom directory"); + } + + #endregion + + #region LoadPersistenceData Tests + + [Test] + public void LoadPersistenceData_WithApplicationStartupDir_LoadsCorrectFile () + { + // Arrange + var preferences = new Preferences + { + SaveLocation = SessionSaveLocation.ApplicationStartupDir + }; + + var originalData = new PersistenceData + { + FileName = _logFileName, + CurrentLine = 123, + FollowTail = true, + FilterVisible = false + }; + + // Save data first + _ = Core.Classes.Persister.Persister.SavePersistenceData(_logFileName, originalData, preferences, _applicationStartupPath); + + // Act + var loadedData = Core.Classes.Persister.Persister.LoadPersistenceData(_logFileName, preferences, _applicationStartupPath); + + // Assert + Assert.That(loadedData, Is.Not.Null, "Should load persistence data"); + Assert.That(loadedData.CurrentLine, Is.EqualTo(123), "Should load correct CurrentLine"); + Assert.That(loadedData.FollowTail, Is.True, "Should load correct FollowTail"); + Assert.That(loadedData.FilterVisible, Is.False, "Should load correct FilterVisible"); + } + + [Test] + public void LoadPersistenceData_WithApplicationStartupDir_FileNotExists_ReturnsNull () + { + // Arrange + var preferences = new Preferences + { + SaveLocation = SessionSaveLocation.ApplicationStartupDir + }; + + var nonExistentFile = Path.Join(_testDirectory, "nonexistent.log"); + + // Act + var loadedData = Core.Classes.Persister.Persister.LoadPersistenceData(nonExistentFile, preferences, _applicationStartupPath); + + // Assert + Assert.That(loadedData, Is.Null, "Should return null when file doesn't exist"); + } + + [Test] + public void LoadPersistenceData_WithApplicationStartupDir_NullApplicationStartupPath_ThrowsArgumentNullException () + { + // Arrange + var preferences = new Preferences + { + SaveLocation = SessionSaveLocation.ApplicationStartupDir + }; + + // Act & Assert + _ = Assert.Throws(() => Core.Classes.Persister.Persister.LoadPersistenceData(_logFileName, preferences, null)); + } + + #endregion + + #region LoadPersistenceDataOptionsOnly Tests + + [Test] + public void LoadPersistenceDataOptionsOnly_WithApplicationStartupDir_LoadsCorrectData () + { + // Arrange + var preferences = new Preferences + { + SaveLocation = SessionSaveLocation.ApplicationStartupDir + }; + + var originalData = new PersistenceData + { + FileName = _logFileName, + MultiFile = true, + MultiFilePattern = "*.log", + FilterAdvanced = true + }; + + // Save data first + _ = Core.Classes.Persister.Persister.SavePersistenceData(_logFileName, originalData, preferences, _applicationStartupPath); + + // Act + var loadedData = Core.Classes.Persister.Persister.LoadPersistenceDataOptionsOnly(_logFileName, preferences, _applicationStartupPath); + + // Assert + Assert.That(loadedData, Is.Not.Null, "Should load persistence data"); + Assert.That(loadedData.MultiFile, Is.True, "Should load correct MultiFile"); + Assert.That(loadedData.MultiFilePattern, Is.EqualTo("*.log"), "Should load correct MultiFilePattern"); + Assert.That(loadedData.FilterAdvanced, Is.True, "Should load correct FilterAdvanced"); + } + + [Test] + public void LoadPersistenceDataOptionsOnly_WithApplicationStartupDir_NullApplicationStartupPath_ThrowsArgumentNullException () + { + // Arrange + var preferences = new Preferences + { + SaveLocation = SessionSaveLocation.ApplicationStartupDir + }; + + // Act & Assert + _ = Assert.Throws(() => Core.Classes.Persister.Persister.LoadPersistenceDataOptionsOnly(_logFileName, preferences, null)); + } + + #endregion + + #region Round-trip Tests + + [Test] + public void RoundTrip_WithApplicationStartupDir_PreservesAllData () + { + // Arrange + var preferences = new Preferences + { + SaveLocation = SessionSaveLocation.ApplicationStartupDir + }; + + var originalData = new PersistenceData + { + FileName = _logFileName, + CurrentLine = 999, + FirstDisplayedLine = 500, + FollowTail = true, + FilterVisible = true, + FilterAdvanced = false, + FilterPosition = 300, + TabName = "Test Tab", + MultiFile = true, + MultiFilePattern = "test*.log", + MultiFileMaxDays = 7, + LineCount = 1000 + }; + + // Act + _ = Core.Classes.Persister.Persister.SavePersistenceData(_logFileName, originalData, preferences, _applicationStartupPath); + var loadedData = Core.Classes.Persister.Persister.LoadPersistenceData(_logFileName, preferences, _applicationStartupPath); + + // Assert + Assert.That(loadedData, Is.Not.Null, "Should load data"); + Assert.That(loadedData.CurrentLine, Is.EqualTo(originalData.CurrentLine), "CurrentLine should match"); + Assert.That(loadedData.FirstDisplayedLine, Is.EqualTo(originalData.FirstDisplayedLine), "FirstDisplayedLine should match"); + Assert.That(loadedData.FollowTail, Is.EqualTo(originalData.FollowTail), "FollowTail should match"); + Assert.That(loadedData.FilterVisible, Is.EqualTo(originalData.FilterVisible), "FilterVisible should match"); + Assert.That(loadedData.FilterAdvanced, Is.EqualTo(originalData.FilterAdvanced), "FilterAdvanced should match"); + Assert.That(loadedData.FilterPosition, Is.EqualTo(originalData.FilterPosition), "FilterPosition should match"); + Assert.That(loadedData.TabName, Is.EqualTo(originalData.TabName), "TabName should match"); + Assert.That(loadedData.MultiFile, Is.EqualTo(originalData.MultiFile), "MultiFile should match"); + Assert.That(loadedData.MultiFilePattern, Is.EqualTo(originalData.MultiFilePattern), "MultiFilePattern should match"); + Assert.That(loadedData.MultiFileMaxDays, Is.EqualTo(originalData.MultiFileMaxDays), "MultiFileMaxDays should match"); + Assert.That(loadedData.LineCount, Is.EqualTo(originalData.LineCount), "LineCount should match"); + } + + [Test] + public void RoundTrip_SwitchingBetweenLocations_WorksCorrectly () + { + // Arrange + var appDirPreferences = new Preferences + { + SaveLocation = SessionSaveLocation.ApplicationStartupDir + }; + + var sameDirPreferences = new Preferences + { + SaveLocation = SessionSaveLocation.SameDir + }; + + var testData = new PersistenceData + { + FileName = _logFileName, + CurrentLine = 42 + }; + + // Act - Save to ApplicationStartupDir + var appDirFileName = Core.Classes.Persister.Persister.SavePersistenceData(_logFileName, testData, appDirPreferences, _applicationStartupPath); + + // Save to SameDir + var sameDirFileName = Core.Classes.Persister.Persister.SavePersistenceData(_logFileName, testData, sameDirPreferences, _applicationStartupPath); + + // Load from ApplicationStartupDir + var loadedFromAppDir = Core.Classes.Persister.Persister.LoadPersistenceData(_logFileName, appDirPreferences, _applicationStartupPath); + + // Load from SameDir + var loadedFromSameDir = Core.Classes.Persister.Persister.LoadPersistenceData(_logFileName, sameDirPreferences, _applicationStartupPath); + + // Assert + Assert.That(appDirFileName, Is.Not.EqualTo(sameDirFileName), "Files should be in different locations"); + Assert.That(loadedFromAppDir, Is.Not.Null, "Should load from app dir"); + Assert.That(loadedFromSameDir, Is.Not.Null, "Should load from same dir"); + Assert.That(loadedFromAppDir.CurrentLine, Is.EqualTo(42), "App dir data should match"); + Assert.That(loadedFromSameDir.CurrentLine, Is.EqualTo(42), "Same dir data should match"); + } + + #endregion + + #region Edge Cases and Error Handling + + [Test] + public void SavePersistenceData_WithApplicationStartupDir_EmptyPath_ThrowsArgumentException () + { + // Arrange + var preferences = new Preferences + { + SaveLocation = SessionSaveLocation.ApplicationStartupDir + }; + + var persistenceData = new PersistenceData + { + FileName = _logFileName + }; + + // Act & Assert + _ = Assert.Throws(() => Core.Classes.Persister.Persister.SavePersistenceData(_logFileName, persistenceData, preferences, string.Empty)); + } + + [Test] + public void SavePersistenceData_WithApplicationStartupDir_WhitespacePath_ThrowsArgumentException () + { + // Arrange + var preferences = new Preferences + { + SaveLocation = SessionSaveLocation.ApplicationStartupDir + }; + + var persistenceData = new PersistenceData + { + FileName = _logFileName + }; + + // Act & Assert + _ = Assert.Throws(() => Core.Classes.Persister.Persister.SavePersistenceData(_logFileName, persistenceData, preferences, " ")); + } + + [Test] + public void SavePersistenceData_WithApplicationStartupDir_SpecialCharactersInPath_HandlesCorrectly () + { + // Arrange + var specialPath = Path.Join(_testDirectory, "Special Path With Spaces & Symbols"); + _ = Directory.CreateDirectory(specialPath); + + var preferences = new Preferences + { + SaveLocation = SessionSaveLocation.ApplicationStartupDir + }; + + var persistenceData = new PersistenceData + { + FileName = _logFileName, + CurrentLine = 100 + }; + + // Act + var savedFileName = Core.Classes.Persister.Persister.SavePersistenceData(_logFileName, persistenceData, preferences, specialPath); + var loadedData = Core.Classes.Persister.Persister.LoadPersistenceData(_logFileName, preferences, specialPath); + + // Assert + Assert.That(File.Exists(savedFileName), Is.True, "Should handle special characters in path"); + Assert.That(loadedData, Is.Not.Null, "Should load from path with special characters"); + Assert.That(loadedData.CurrentLine, Is.EqualTo(100), "Data should be preserved"); + } + + [Test] + public void SavePersistenceData_WithApplicationStartupDir_UnicodeCharactersInPath_HandlesCorrectly () + { + // Arrange + var unicodePath = Path.Join(_testDirectory, "Пути_日本語_Ελληνικά"); + _ = Directory.CreateDirectory(unicodePath); + + var preferences = new Preferences + { + SaveLocation = SessionSaveLocation.ApplicationStartupDir + }; + + var persistenceData = new PersistenceData + { + FileName = _logFileName, + CurrentLine = 200 + }; + + // Act + var savedFileName = Core.Classes.Persister.Persister.SavePersistenceData(_logFileName, persistenceData, preferences, unicodePath); + var loadedData = Core.Classes.Persister.Persister.LoadPersistenceData(_logFileName, preferences, unicodePath); + + // Assert + Assert.That(File.Exists(savedFileName), Is.True, "Should handle unicode characters in path"); + Assert.That(loadedData, Is.Not.Null, "Should load from path with unicode characters"); + Assert.That(loadedData.CurrentLine, Is.EqualTo(200), "Data should be preserved"); + } + + [Test] + public void SavePersistenceData_WithApplicationStartupDir_LongPath_HandlesCorrectly () + { + // Arrange - Create a deep directory structure + var longPath = _applicationStartupPath; + for (int i = 0; i < 10; i++) + { + longPath = Path.Join(longPath, $"SubDirectory{i}"); + } + + _ = Directory.CreateDirectory(longPath); + + var preferences = new Preferences + { + SaveLocation = SessionSaveLocation.ApplicationStartupDir + }; + + var persistenceData = new PersistenceData + { + FileName = _logFileName, + CurrentLine = 300 + }; + + // Act + var savedFileName = Core.Classes.Persister.Persister.SavePersistenceData(_logFileName, persistenceData, preferences, longPath); + var loadedData = Core.Classes.Persister.Persister.LoadPersistenceData(_logFileName, preferences, longPath); + + // Assert + Assert.That(File.Exists(savedFileName), Is.True, "Should handle long paths"); + Assert.That(loadedData, Is.Not.Null, "Should load from long path"); + Assert.That(loadedData.CurrentLine, Is.EqualTo(300), "Data should be preserved"); + } + + #endregion + + #region Backward Compatibility Tests + + [Test] + public void LoadPersistenceData_WithoutApplicationStartupPath_StillWorksForOtherLocations () + { + // Arrange + var preferences = new Preferences + { + SaveLocation = SessionSaveLocation.SameDir + }; + + var persistenceData = new PersistenceData + { + FileName = _logFileName, + CurrentLine = 50 + }; + + // Act - Save with SameDir (should not use applicationStartupPath) + _ = Core.Classes.Persister.Persister.SavePersistenceData(_logFileName, persistenceData, preferences, _applicationStartupPath); + var loadedData = Core.Classes.Persister.Persister.LoadPersistenceData(_logFileName, preferences, _applicationStartupPath); + + // Assert + Assert.That(loadedData, Is.Not.Null, "Should work for non-ApplicationStartupDir locations"); + Assert.That(loadedData.CurrentLine, Is.EqualTo(50), "Data should be preserved"); + } + + #endregion + + #region Concurrency Tests + + [Test] + public void SavePersistenceData_ConcurrentSaves_ToApplicationStartupDir_HandlesCorrectly () + { + // Arrange + var preferences = new Preferences + { + SaveLocation = SessionSaveLocation.ApplicationStartupDir + }; + + var tasks = new List>(); + var logFiles = new List(); + + // Create multiple log files + for (int i = 0; i < 5; i++) + { + var logFile = Path.Join(_testDirectory, $"concurrent_test_{i}.log"); + File.WriteAllText(logFile, $"Test log {i}"); + logFiles.Add(logFile); + } + + // Act - Save persistence data concurrently + foreach (var logFile in logFiles) + { + var index = logFiles.IndexOf(logFile); + tasks.Add(Task.Run(() => + { + var data = new PersistenceData + { + FileName = logFile, + CurrentLine = index * 100 + }; + return Core.Classes.Persister.Persister.SavePersistenceData(logFile, data, preferences, _applicationStartupPath); + })); + } + + Task.WaitAll(tasks.ToArray()); + + // Assert - Verify all files were saved correctly + for (int i = 0; i < logFiles.Count; i++) + { + var loadedData = Core.Classes.Persister.Persister.LoadPersistenceData(logFiles[i], preferences, _applicationStartupPath); + Assert.That(loadedData, Is.Not.Null, $"Should load data for file {i}"); + Assert.That(loadedData.CurrentLine, Is.EqualTo(i * 100), $"Data should match for file {i}"); + } + } + + #endregion + + #region Directory Creation Tests + + [Test] + public void SavePersistenceData_ApplicationStartupDirNotExists_CreatesDirectory () + { + // Arrange + var nonExistentAppPath = Path.Join(_testDirectory, "NonExistentAppPath"); + // Don't create the directory - let Persister create it + + var preferences = new Preferences + { + SaveLocation = SessionSaveLocation.ApplicationStartupDir + }; + + var persistenceData = new PersistenceData + { + FileName = _logFileName, + CurrentLine = 77 + }; + + // Act + var savedFileName = Core.Classes.Persister.Persister.SavePersistenceData(_logFileName, persistenceData, preferences, nonExistentAppPath); + + // Assert + var expectedDirectory = Path.Join(nonExistentAppPath, "sessionFiles"); + Assert.That(Directory.Exists(expectedDirectory), Is.True, "Should create sessionFiles directory"); + Assert.That(File.Exists(savedFileName), Is.True, "Should create persistence file"); + } + + #endregion +} diff --git a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs index 5022d365..5ddf3600 100644 --- a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs +++ b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs @@ -2405,7 +2405,7 @@ private bool LoadPersistenceOptions () try { var persistenceData = ForcedPersistenceFileName == null - ? Persister.LoadPersistenceDataOptionsOnly(FileName, Preferences) + ? Persister.LoadPersistenceDataOptionsOnly(FileName, Preferences, Application.StartupPath) : Persister.LoadPersistenceDataOptionsOnlyFromFixedFile(ForcedPersistenceFileName); if (persistenceData == null) @@ -2503,7 +2503,7 @@ private void LoadPersistenceData () try { var persistenceData = ForcedPersistenceFileName == null - ? Persister.LoadPersistenceData(FileName, Preferences) + ? Persister.LoadPersistenceData(FileName, Preferences, Application.StartupPath) : Persister.LoadPersistenceDataFromFixedFile(ForcedPersistenceFileName); if (persistenceData == null) @@ -6240,7 +6240,7 @@ public string SavePersistenceDataAndReturnFileName (bool force) var persistenceData = GetPersistenceData(); return ForcedPersistenceFileName == null - ? Persister.SavePersistenceData(FileName, persistenceData, Preferences) + ? Persister.SavePersistenceData(FileName, persistenceData, Preferences, Application.StartupPath) : Persister.SavePersistenceDataWithFixedName(ForcedPersistenceFileName, persistenceData); } catch (IOException e) diff --git a/src/LogExpert.sln b/src/LogExpert.sln index 6ee7ac5a..2d27b6eb 100644 --- a/src/LogExpert.sln +++ b/src/LogExpert.sln @@ -94,6 +94,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GithubActions", "GithubActi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LogExpert.Configuration", "LogExpert.Configuration\LogExpert.Configuration.csproj", "{9EBCD259-B704-4E1B-81D9-A9DCFD9F62DF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LogExpert.Persister.Tests", "LogExpert.Persister.Tests\LogExpert.Persister.Tests.csproj", "{CAD17410-CE8C-4FE5-91DE-1B3DE2945135}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -198,6 +200,10 @@ Global {9EBCD259-B704-4E1B-81D9-A9DCFD9F62DF}.Debug|Any CPU.Build.0 = Debug|Any CPU {9EBCD259-B704-4E1B-81D9-A9DCFD9F62DF}.Release|Any CPU.ActiveCfg = Release|Any CPU {9EBCD259-B704-4E1B-81D9-A9DCFD9F62DF}.Release|Any CPU.Build.0 = Release|Any CPU + {CAD17410-CE8C-4FE5-91DE-1B3DE2945135}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CAD17410-CE8C-4FE5-91DE-1B3DE2945135}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CAD17410-CE8C-4FE5-91DE-1B3DE2945135}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CAD17410-CE8C-4FE5-91DE-1B3DE2945135}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -216,6 +222,7 @@ Global {FBFB598D-B94A-4AD3-A355-0D5A618CEEE3} = {848C24BA-BEBA-48EC-90E6-526ECAB6BB4A} {27EF66B7-C90C-7D5C-BD53-113DB43DF578} = {848C24BA-BEBA-48EC-90E6-526ECAB6BB4A} {39822C1B-E4C6-40F3-86C4-74C68BDEF3D0} = {DE6375A4-B4C4-4620-8FFB-B9D5A4E21144} + {CAD17410-CE8C-4FE5-91DE-1B3DE2945135} = {848C24BA-BEBA-48EC-90E6-526ECAB6BB4A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {15924D5F-B90B-4BC7-9E7D-BCCB62EBABAD} diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs index d257cc80..53d95c12 100644 --- a/src/PluginRegistry/PluginHashGenerator.Generated.cs +++ b/src/PluginRegistry/PluginHashGenerator.Generated.cs @@ -10,7 +10,7 @@ public static partial class PluginValidator { /// /// Gets pre-calculated SHA256 hashes for built-in plugins. - /// Generated: 2025-12-02 10:05:09 UTC + /// Generated: 2025-12-03 10:17:08 UTC /// Configuration: Release /// Plugin count: 21 /// @@ -18,27 +18,27 @@ public static Dictionary GetBuiltInPluginHashes() { return new Dictionary(StringComparer.OrdinalIgnoreCase) { - ["AutoColumnizer.dll"] = "355042D70A3A54B615E28AFC3622A5F2549A6648668C216EDFF653BF49B993A3", + ["AutoColumnizer.dll"] = "DEFEE4450E0B6EC0848902A7BEEC141AAE89DE8B71F92F9EBD2206632D96F17A", ["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", ["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", - ["CsvColumnizer.dll"] = "B13B074D7D4D9610289A529EF630E8ABA55CD60FBB6E67053C79A4DCAEC16298", - ["CsvColumnizer.dll (x86)"] = "B13B074D7D4D9610289A529EF630E8ABA55CD60FBB6E67053C79A4DCAEC16298", - ["DefaultPlugins.dll"] = "85FF89A3B59C2143D114690BA0FF6A373552FC14E1EB948F186DFBDB4F748076", - ["FlashIconHighlighter.dll"] = "B055A3128D25F453E646D1E394BC9B34AA3F3660AF6F825C866C906B6906A336", - ["GlassfishColumnizer.dll"] = "4C69DBEE004465370EBA0B33AB179147FFDA2C972633A96B2FD5A3F29257CAA6", - ["JsonColumnizer.dll"] = "A03F65776774D44113A348D412C690794B17B7B86B7180A9A02173BC57B6DD2E", - ["JsonCompactColumnizer.dll"] = "7A4B468635FAB8098283F54E139F325A035EDE439F506BD9F058CC073645C449", - ["Log4jXmlColumnizer.dll"] = "AF15F139EAAE97BC4ECE2E98CDA087FBECBC1BD13E0D807017447AC7AB846BF5", - ["LogExpert.Core.dll"] = "C26B73709B8BFD40E17ECF6111FF28EAE6BD05EB77A5715DE935DD0B90C8A607", + ["CsvColumnizer.dll"] = "46827846AD2C2A1BDF6600A5BD7080BC8799B54C81329CDFB23015BDCBF7A480", + ["CsvColumnizer.dll (x86)"] = "46827846AD2C2A1BDF6600A5BD7080BC8799B54C81329CDFB23015BDCBF7A480", + ["DefaultPlugins.dll"] = "B6DC411F1394D7C2A83B242990E2E4356B0EF523CB518B9FF5658BBC2E05BF5F", + ["FlashIconHighlighter.dll"] = "70545A26BB7D433EE3B2735AC38DE233CB5BF4B1F0623E716323C9C95242072B", + ["GlassfishColumnizer.dll"] = "FEB269CB14C068698F19ABE3B93C4F5E2DC11116713509FD718C1A0929CDE716", + ["JsonColumnizer.dll"] = "99E86FC39F0DF73550E184D50939BB468EB11930E72CBE78353DC9733B9D7365", + ["JsonCompactColumnizer.dll"] = "630ED6696B8976596EC2A20228D5F8E9EC6FA94CB544936196C8E9849F692F5A", + ["Log4jXmlColumnizer.dll"] = "EB1DF83134242B6A38A354ED50AC16EC25EB9AF02123620CB89209A46CDD0102", + ["LogExpert.Core.dll"] = "16AF8E6719D60D5C4B3FFDB63FAA9F81478C1A2445070185E28B3259C5AC0786", ["Microsoft.Extensions.DependencyInjection.Abstractions.dll"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93", ["Microsoft.Extensions.DependencyInjection.Abstractions.dll (x86)"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93", ["Microsoft.Extensions.Logging.Abstractions.dll"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D", ["Microsoft.Extensions.Logging.Abstractions.dll (x86)"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D", - ["RegexColumnizer.dll"] = "526CF05814D0F9715EB7CB7DD6E1D5BE73C8A0691AB6294723B30ABCD6A1948B", - ["SftpFileSystem.dll"] = "F8F105F845F1EAC738BDA791C12309E6A747711222F643319CCBB5686DC3D80A", - ["SftpFileSystem.dll (x86)"] = "1D2F466A6D0FC3D45CB0EFF5671A23658A183E929053FF04079A5934FAF6F47D", - ["SftpFileSystem.Resources.dll"] = "53D75191BD22A5DF63ED597450DCF04D5D2518AAE798F83F31C55C21CC5F9918", - ["SftpFileSystem.Resources.dll (x86)"] = "53D75191BD22A5DF63ED597450DCF04D5D2518AAE798F83F31C55C21CC5F9918", + ["RegexColumnizer.dll"] = "98E8A88829BA43CE7E763D197145F1A0863D891F6F274528B8EFE6EF64443063", + ["SftpFileSystem.dll"] = "C98EA0176B51E21F685F53EF0CD6CA3B2B4108700EEC19BC0A85019B32BB6067", + ["SftpFileSystem.dll (x86)"] = "F65FBD5E63A7D4E801366B5BE043FDA1B12762C21668B8B00ADA3727E469DF9F", + ["SftpFileSystem.Resources.dll"] = "B9AF09F4B77E6CD439240DFC94332AD909500478963BFB049BAD6D30A3842113", + ["SftpFileSystem.Resources.dll (x86)"] = "B9AF09F4B77E6CD439240DFC94332AD909500478963BFB049BAD6D30A3842113", }; }