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",
};
}