diff --git a/src/ColumnizerLib/ILogFileInfo.cs b/src/ColumnizerLib/ILogFileInfo.cs index 6a10b530c..ed5a36309 100644 --- a/src/ColumnizerLib/ILogFileInfo.cs +++ b/src/ColumnizerLib/ILogFileInfo.cs @@ -51,6 +51,12 @@ public interface ILogFileInfo /// bool FileExists { get; } + /// + /// Gets the time when the file was last written, in UTC, or when the file system plugin + /// cannot provide it. + /// + DateTime? LastWriteTimeUtc => null; + /// /// The interval (in ms) LogExpert should check for file changes. The property is checked by LogExpert repeatedly in the loop which /// checks for file changes. So you can adjust the poll interval as needed. E.g. you can lower the interval when many changes occur and diff --git a/src/LogExpert.Core/Classes/Log/RolloverFilenameHandler.cs b/src/LogExpert.Core/Classes/Log/RolloverFilenameHandler.cs index 1c4fe52c9..153618015 100644 --- a/src/LogExpert.Core/Classes/Log/RolloverFilenameHandler.cs +++ b/src/LogExpert.Core/Classes/Log/RolloverFilenameHandler.cs @@ -21,6 +21,7 @@ public class RolloverFilenameHandler private readonly RolloverFilenameBuilder _filenameBuilder; private readonly ILogFileInfo _logFileInfo; private readonly MultiFileOptions _options; + private readonly TimeProvider _timeProvider; #endregion @@ -31,10 +32,12 @@ public class RolloverFilenameHandler /// /// The complete path of the logfile /// Multifile option (e.g. format pattern) - public RolloverFilenameHandler(ILogFileInfo logFileInfo, MultiFileOptions options) + /// Time source used to calculate the file-age cutoff. + public RolloverFilenameHandler(ILogFileInfo logFileInfo, MultiFileOptions options, TimeProvider? timeProvider = null) { _options = options; _logFileInfo = logFileInfo; + _timeProvider = timeProvider ?? TimeProvider.System; _filenameBuilder = new RolloverFilenameBuilder(_options.FormatPattern); _filenameBuilder.SetFileName(logFileInfo.FileName); } @@ -69,12 +72,18 @@ public LinkedList GetNameList(IPluginRegistry pluginRegistry) _filenameBuilder.Index += 1; fileName = _filenameBuilder.BuildFileName(); filePath = _logFileInfo.DirectoryName + _logFileInfo.DirectorySeparatorChar + fileName;//TODO: Change to Directory.Combine - if (FileExists(filePath, pluginRegistry)) + var result = GetCandidateResult(filePath, pluginRegistry); + if (result == CandidateResult.Include) { _ = fileList.AddFirst(filePath); found = true; continue; } + + if (result == CandidateResult.TooOld) + { + return fileList; + } } // if file with index isn't found or no index is in format pattern, decrement the current date @@ -87,13 +96,19 @@ public LinkedList GetNameList(IPluginRegistry pluginRegistry) _filenameBuilder.DecrementDate(); fileName = _filenameBuilder.BuildFileName(); filePath = _logFileInfo.DirectoryName + _logFileInfo.DirectorySeparatorChar + fileName;//TODO: Change to Directory.Combine - if (FileExists(filePath, pluginRegistry)) + var result = GetCandidateResult(filePath, pluginRegistry); + if (result == CandidateResult.Include) { _ = fileList.AddFirst(filePath); found = true; break; } + if (result == CandidateResult.TooOld) + { + return fileList; + } + tryCounter++; } } @@ -106,17 +121,36 @@ public LinkedList GetNameList(IPluginRegistry pluginRegistry) #region Private Methods - private bool FileExists(string filePath, IPluginRegistry pluginRegistry) + private CandidateResult GetCandidateResult(string filePath, IPluginRegistry pluginRegistry) { var fs = pluginRegistry.FindFileSystemForUri(filePath); if(fs == null) { - return false; + return CandidateResult.Missing; } var info = fs.GetLogfileInfo(filePath); - return info is not null && info.FileExists; + if (info is null || !info.FileExists) + { + return CandidateResult.Missing; + } + + var maxFileAgeDays = MultiFileOptions.ClampMaxFileAgeDays(_options.MaxFileAgeDays); + if (maxFileAgeDays == 0 || info.LastWriteTimeUtc is not { } lastWriteTimeUtc) + { + return CandidateResult.Include; + } + + var cutoffUtc = _timeProvider.GetUtcNow().UtcDateTime - TimeSpan.FromDays(maxFileAgeDays); + return lastWriteTimeUtc >= cutoffUtc ? CandidateResult.Include : CandidateResult.TooOld; + } + + private enum CandidateResult + { + Missing, + Include, + TooOld } #endregion diff --git a/src/LogExpert.Core/Classes/Persister/PersistenceData.cs b/src/LogExpert.Core/Classes/Persister/PersistenceData.cs index 37e80e377..fb72943c0 100644 --- a/src/LogExpert.Core/Classes/Persister/PersistenceData.cs +++ b/src/LogExpert.Core/Classes/Persister/PersistenceData.cs @@ -71,6 +71,8 @@ public class PersistenceData public int MultiFileMaxDays { get; set; } + public int MultiFileMaxFileAgeDays { get; set; } + public List MultiFileNames { get; set; } = []; public string MultiFilePattern { get; set; } diff --git a/src/LogExpert.Core/Classes/Persister/SessionFileComposer.cs b/src/LogExpert.Core/Classes/Persister/SessionFileComposer.cs index 490aa8c83..5482075e2 100644 --- a/src/LogExpert.Core/Classes/Persister/SessionFileComposer.cs +++ b/src/LogExpert.Core/Classes/Persister/SessionFileComposer.cs @@ -36,6 +36,7 @@ public static PersistenceData Compose (SessionSnapshot snapshot) FilterSaveListVisible = snapshot.FilterSaveListVisible, MultiFile = snapshot.MultiFile, MultiFileMaxDays = snapshot.MultiFileMaxDays, + MultiFileMaxFileAgeDays = snapshot.MultiFileMaxFileAgeDays, MultiFilePattern = snapshot.MultiFilePattern, TabName = snapshot.TabName, HighlightGroupName = snapshot.HighlightGroupName, @@ -83,6 +84,7 @@ public static SessionSnapshot Decompose (PersistenceData persistenceData) FilterSaveListVisible = persistenceData.FilterSaveListVisible, MultiFile = persistenceData.MultiFile, MultiFileMaxDays = persistenceData.MultiFileMaxDays, + MultiFileMaxFileAgeDays = persistenceData.MultiFileMaxFileAgeDays, MultiFilePattern = persistenceData.MultiFilePattern, TabName = persistenceData.TabName, HighlightGroupName = persistenceData.HighlightGroupName, diff --git a/src/LogExpert.Core/Classes/Persister/SessionSnapshot.cs b/src/LogExpert.Core/Classes/Persister/SessionSnapshot.cs index bec804861..462309012 100644 --- a/src/LogExpert.Core/Classes/Persister/SessionSnapshot.cs +++ b/src/LogExpert.Core/Classes/Persister/SessionSnapshot.cs @@ -43,6 +43,8 @@ public class SessionSnapshot public int MultiFileMaxDays { get; set; } + public int MultiFileMaxFileAgeDays { get; set; } + public string MultiFilePattern { get; set; } public string TabName { get; set; } diff --git a/src/LogExpert.Core/Entities/MultifileOptions.cs b/src/LogExpert.Core/Entities/MultifileOptions.cs index b9ab6d782..4d5d6f292 100644 --- a/src/LogExpert.Core/Entities/MultifileOptions.cs +++ b/src/LogExpert.Core/Entities/MultifileOptions.cs @@ -5,8 +5,24 @@ public class MultiFileOptions { #region Properties + /// + /// The largest file age exposed by the UI and accepted by the rollover reader, in days. + /// + public const int MAX_FILE_AGE_DAYS = 3650; + public int MaxDayTry { get; set; } = 3; + /// + /// Gets or sets the maximum age, in days, of rollover files loaded into a MultiFile. Zero disables the limit. + /// Values greater than are treated as that maximum. + /// + public int MaxFileAgeDays { get; set; } + + public static int ClampMaxFileAgeDays (int value) + { + return Math.Clamp(value, 0, MAX_FILE_AGE_DAYS); + } + public string FormatPattern { get; set; } = "*$J(.)"; #endregion diff --git a/src/LogExpert.Persister.Tests/PersisterTests.cs b/src/LogExpert.Persister.Tests/PersisterTests.cs index 52e0dcb78..b07be02cd 100644 --- a/src/LogExpert.Persister.Tests/PersisterTests.cs +++ b/src/LogExpert.Persister.Tests/PersisterTests.cs @@ -357,6 +357,7 @@ public void RoundTrip_WithApplicationStartupDir_PreservesAllData () MultiFile = true, MultiFilePattern = "test*.log", MultiFileMaxDays = 7, + MultiFileMaxFileAgeDays = 5, LineCount = 1000, CellSelectMode = true }; @@ -377,6 +378,8 @@ public void RoundTrip_WithApplicationStartupDir_PreservesAllData () 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.MultiFileMaxFileAgeDays, Is.EqualTo(originalData.MultiFileMaxFileAgeDays), + "MultiFileMaxFileAgeDays should match"); Assert.That(loadedData.LineCount, Is.EqualTo(originalData.LineCount), "LineCount should match"); Assert.That(loadedData.CellSelectMode, Is.EqualTo(originalData.CellSelectMode), "CellSelectMode should match"); } diff --git a/src/LogExpert.Persister.Tests/PersisterXmlMalformedTests.cs b/src/LogExpert.Persister.Tests/PersisterXmlMalformedTests.cs index ee6aaba95..b4546e5f7 100644 --- a/src/LogExpert.Persister.Tests/PersisterXmlMalformedTests.cs +++ b/src/LogExpert.Persister.Tests/PersisterXmlMalformedTests.cs @@ -44,6 +44,7 @@ public void Load_FileElementWithoutOptions_ReturnsDefaults () Assert.That(data.MultiFile, Is.False); Assert.That(data.MultiFilePattern, Is.Null); Assert.That(data.MultiFileMaxDays, Is.Zero); + Assert.That(data.MultiFileMaxFileAgeDays, Is.Zero); Assert.That(data.FilterVisible, Is.False); // A missing means "nothing was persisted", so every option keeps its diff --git a/src/LogExpert.Persister.Tests/SessionFileComposerTests.cs b/src/LogExpert.Persister.Tests/SessionFileComposerTests.cs index b1c97af82..f820d7165 100644 --- a/src/LogExpert.Persister.Tests/SessionFileComposerTests.cs +++ b/src/LogExpert.Persister.Tests/SessionFileComposerTests.cs @@ -172,6 +172,7 @@ private static SessionSnapshot CreateFullSnapshot (int filterTabDepth = 2) FilterSaveListVisible = true, MultiFile = true, MultiFileMaxDays = 7, + MultiFileMaxFileAgeDays = 5, MultiFilePattern = "app*.log", TabName = "My Tab", HighlightGroupName = "Errors", @@ -247,6 +248,7 @@ private static PersistenceData CreateCarriedPersistenceData (int filterTabDepth) FilterSaveListVisible = true, MultiFile = true, MultiFileMaxDays = 7, + MultiFileMaxFileAgeDays = 5, MultiFilePattern = "app*.log", TabName = "My Tab", HighlightGroupName = "Errors", @@ -422,6 +424,28 @@ public void DiskTrip_ComposePersistLoadDecompose_ReturnsEqualSnapshot () Assert.That(SerializeForComparison(roundTripped), Is.EqualTo(SerializeForComparison(snapshot))); } + [Test] + public void Decompose_LegacyJsonWithoutMaxFileAgeDays_DefaultsToOff () + { + var sessionFileName = Path.Join(_testDirectory, "legacy-json.lxp"); + File.WriteAllText(sessionFileName, """ + { + "MultiFile": true, + "MultiFileMaxDays": 7, + "MultiFilePattern": "*$J(.)" + } + """); + + var loaded = Core.Classes.Persister.Persister.Load(sessionFileName); + var snapshot = SessionFileComposer.Decompose(loaded); + + Assert.Multiple(() => + { + Assert.That(loaded.MultiFileMaxFileAgeDays, Is.Zero); + Assert.That(snapshot.MultiFileMaxFileAgeDays, Is.Zero); + }); + } + #endregion #region Window filter (issue #666) @@ -490,6 +514,8 @@ public void Decompose_LegacyXmlSessionFileOnDisk_TakesTheFirstFilterAsTheWindowF Assert.That(snapshot.FilterParams.SearchText, Is.EqualTo("the first global filter")); Assert.That(snapshot.FilterParams.SpreadBefore, Is.EqualTo(1), "the restored entry must be the first one, whole — the second carries SpreadBefore 2"); + Assert.That(snapshot.MultiFileMaxFileAgeDays, Is.Zero, + "legacy XML Session Files without the new field must keep age filtering disabled"); Assert.That(snapshot.FilterParams.Color, Is.EqualTo(Color.Black), "Color is the field the legacy payload writes as \"Black\" — losing it means the entry was not really read"); } diff --git a/src/LogExpert.Resources/Resources.Designer.cs b/src/LogExpert.Resources/Resources.Designer.cs index 6bf4905df..625847afc 100644 --- a/src/LogExpert.Resources/Resources.Designer.cs +++ b/src/LogExpert.Resources/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace LogExpert { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class Resources { @@ -416,7 +416,7 @@ public static string Columnizer_SnapshotUnavailable { return ResourceManager.GetString("Columnizer_SnapshotUnavailable", resourceCulture); } } - + /// /// Looks up a localized string similar to Timestamp selector. /// @@ -1081,6 +1081,123 @@ public static string HighlightDialog_UI_Button_SelectPlugin { } } + /// + /// Looks up a localized string similar to Create a new highlight item (opens the editor dialog). + /// + public static string HighlightDialog_UI_Button_ToolTip_btnAdd { + get { + return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnAdd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copy the selected highlight into a new one (opens the editor dialog). + /// + public static string HighlightDialog_UI_Button_ToolTip_btnCopy { + get { + return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnCopy", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copy the current highlight group into a new one. + /// + public static string HighlightDialog_UI_Button_ToolTip_btnCopyGroup { + get { + return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnCopyGroup", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Delete the current highlight. + /// + public static string HighlightDialog_UI_Button_ToolTip_btnDelete { + get { + return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnDelete", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Delete the current highlight group. + /// + public static string HighlightDialog_UI_Button_ToolTip_btnDeleteGroup { + get { + return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnDeleteGroup", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Edit the selected highlight in the editor dialog. + /// + public static string HighlightDialog_UI_Button_ToolTip_btnEdit { + get { + return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnEdit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Export highlight groups. + /// + public static string HighlightDialog_UI_Button_ToolTip_btnExportGroup { + get { + return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnExportGroup", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Import highlight groups. + /// + public static string HighlightDialog_UI_Button_ToolTip_btnImportGroup { + get { + return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnImportGroup", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Move the current highlight one position down. + /// + public static string HighlightDialog_UI_Button_ToolTip_btnMoveDown { + get { + return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnMoveDown", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Move the current highlight group one position down. + /// + public static string HighlightDialog_UI_Button_ToolTip_btnMoveGroupDown { + get { + return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnMoveGroupDown", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Move the current highlight group one position up. + /// + public static string HighlightDialog_UI_Button_ToolTip_btnMoveGroupUp { + get { + return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnMoveGroupUp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Move the current highlight one position up. + /// + public static string HighlightDialog_UI_Button_ToolTip_btnMoveUp { + get { + return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnMoveUp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Create a new empty highlight group. + /// + public static string HighlightDialog_UI_Button_ToolTip_btnNewGroup { + get { + return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnNewGroup", resourceCulture); + } + } + /// /// Looks up a localized string similar to Change selection highlight color.... /// @@ -1189,6 +1306,15 @@ public static string HighlightDialog_UI_CheckBox_WordMatch { } } + /// + /// Looks up a localized string similar to Choose a group to create different highlight settings. Type in a name to change the name of a group.. + /// + public static string HighlightDialog_UI_ComboBox_ToolTip_comboBoxGroups { + get { + return ResourceManager.GetString("HighlightDialog_UI_ComboBox_ToolTip_comboBoxGroups", resourceCulture); + } + } + /// /// Looks up a localized string similar to [Default]. /// @@ -4208,7 +4334,7 @@ public static string MarkerBar_Bookmarks { return ResourceManager.GetString("MarkerBar_Bookmarks", resourceCulture); } } - + /// /// Looks up a localized string similar to Clear search. /// @@ -4217,7 +4343,7 @@ public static string MarkerBar_ClearSearch { return ResourceManager.GetString("MarkerBar_ClearSearch", resourceCulture); } } - + /// /// Looks up a localized string similar to Discovering markers…. /// @@ -4226,7 +4352,7 @@ public static string MarkerBar_Discovering { return ResourceManager.GetString("MarkerBar_Discovering", resourceCulture); } } - + /// /// Looks up a localized string similar to Filter hits. /// @@ -4235,7 +4361,7 @@ public static string MarkerBar_FilterHits { return ResourceManager.GetString("MarkerBar_FilterHits", resourceCulture); } } - + /// /// Looks up a localized string similar to Highlights. /// @@ -4244,7 +4370,7 @@ public static string MarkerBar_Highlights { return ResourceManager.GetString("MarkerBar_Highlights", resourceCulture); } } - + /// /// Looks up a localized string similar to Could not read log line {0} while discovering markers.. /// @@ -4253,7 +4379,7 @@ public static string MarkerBar_LineUnavailable { return ResourceManager.GetString("MarkerBar_LineUnavailable", resourceCulture); } } - + /// /// Looks up a localized string similar to Marker discovery failed: {0}. /// @@ -4262,7 +4388,7 @@ public static string MarkerBar_ScanFailed { return ResourceManager.GetString("MarkerBar_ScanFailed", resourceCulture); } } - + /// /// Looks up a localized string similar to Search hits. /// @@ -4271,7 +4397,7 @@ public static string MarkerBar_SearchHits { return ResourceManager.GetString("MarkerBar_SearchHits", resourceCulture); } } - + /// /// Looks up a localized string similar to Marker bar. /// @@ -4280,7 +4406,7 @@ public static string MarkerBar_Title { return ResourceManager.GetString("MarkerBar_Title", resourceCulture); } } - + /// /// Looks up a localized string similar to {0}: lines {1}–{2}, {3} matching lines. /// @@ -4289,7 +4415,7 @@ public static string MarkerBar_ToolTip { return ResourceManager.GetString("MarkerBar_ToolTip", resourceCulture); } } - + /// /// Looks up a localized string similar to Close existing tabs. /// @@ -4435,11 +4561,29 @@ public static string MultiFileMaskDialog_UI_Label_FileNamePattern { } /// - /// Looks up a localized string similar to Max days:. + /// Looks up a localized string similar to Max day gap:. /// - public static string MultiFileMaskDialog_UI_Label_MaxDays { + public static string MultiFileMaskDialog_UI_Label_MaxDayGap { get { - return ResourceManager.GetString("MultiFileMaskDialog_UI_Label_MaxDays", resourceCulture); + return ResourceManager.GetString("MultiFileMaskDialog_UI_Label_MaxDayGap", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Max file age (days):. + /// + public static string MultiFileMaskDialog_UI_Label_MaxFileAgeDays { + get { + return ResourceManager.GetString("MultiFileMaskDialog_UI_Label_MaxFileAgeDays", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to 0 = Off. + /// + public static string MultiFileMaskDialog_UI_Label_MaxFileAgeDaysHint { + get { + return ResourceManager.GetString("MultiFileMaskDialog_UI_Label_MaxFileAgeDaysHint", resourceCulture); } } @@ -4456,16 +4600,24 @@ public static string MultiFileMaskDialog_UI_Label_SettingsFor { /// Looks up a localized string similar to Pattern syntax: /// ///* = any characters (wildcard) - ///$D(&lt;date&gt;) = Date pattern + ///$D(<date>) = Date pattern ///$I = File index number ///$J = File index number, hidden when zero - ///$J(&lt;prefix&gt;) = Like $J, but adding &lt;prefix&gt; when non-zero + ///$J(<prefix>) = Like $J, but adding <prefix> when non-zero + /// + ///Examples: + ///*$J(.) → app.log, app.log.1, app.log.2 + ///*$J(.).log → app.log, app.1.log, app.2.log /// - ///&lt;date&gt;: + ///<date>: ///DD = day ///MM = month ///YY[YY] = year - ///all other chars will be used as given. + ///all other chars will be used as given + /// + ///MultiFile limits: + ///Max day gap = For $D patterns, consecutive days without a log file to skip before giving up + ///Max file age [rest of string was truncated]";. /// public static string MultiFileMaskDialog_UI_Label_SyntaxHelp { get { @@ -4564,7 +4716,7 @@ public static string ParamRequesterDialog_UI_Label_ValueForParameter { } /// - /// Looks up a localized string similar to &Value for "{0}":. + /// Looks up a localized string similar to &Value for "{0}":. /// public static string ParamRequesterDialog_UI_Label_ValueForParameterNamed { get { @@ -5642,7 +5794,7 @@ public static string SettingsDialog_UI_CheckBox_checkBoxShowBookmarkMarkers { return ResourceManager.GetString("SettingsDialog_UI_CheckBox_checkBoxShowBookmarkMarkers", resourceCulture); } } - + /// /// Looks up a localized string similar to Show Error Message?. /// @@ -5660,7 +5812,7 @@ public static string SettingsDialog_UI_CheckBox_checkBoxShowFilterMarkers { return ResourceManager.GetString("SettingsDialog_UI_CheckBox_checkBoxShowFilterMarkers", resourceCulture); } } - + /// /// Looks up a localized string similar to Show highlight markers. /// @@ -5669,7 +5821,7 @@ public static string SettingsDialog_UI_CheckBox_checkBoxShowHighlightMarkers { return ResourceManager.GetString("SettingsDialog_UI_CheckBox_checkBoxShowHighlightMarkers", resourceCulture); } } - + /// /// Looks up a localized string similar to Show marker bar. /// @@ -5678,7 +5830,7 @@ public static string SettingsDialog_UI_CheckBox_checkBoxShowMarkerBar { return ResourceManager.GetString("SettingsDialog_UI_CheckBox_checkBoxShowMarkerBar", resourceCulture); } } - + /// /// Looks up a localized string similar to Show search markers. /// @@ -5687,7 +5839,7 @@ public static string SettingsDialog_UI_CheckBox_checkBoxShowSearchMarkers { return ResourceManager.GetString("SettingsDialog_UI_CheckBox_checkBoxShowSearchMarkers", resourceCulture); } } - + /// /// Looks up a localized string similar to Allow only 1 Instance. /// @@ -6193,29 +6345,47 @@ public static string SettingsDialog_UI_Label_labelLinesPerBlock { } /// - /// Looks up a localized string similar to Max days:. + /// Looks up a localized string similar to Max day gap:. /// - public static string SettingsDialog_UI_Label_labelMaxDays { + public static string SettingsDialog_UI_Label_labelMaxDayGap { get { - return ResourceManager.GetString("SettingsDialog_UI_Label_labelMaxDays", resourceCulture); + return ResourceManager.GetString("SettingsDialog_UI_Label_labelMaxDayGap", resourceCulture); } } /// - /// Looks up a localized string similar to Maximum filter entries. + /// Looks up a localized string similar to Maximum Display Length (restart required). /// - public static string SettingsDialog_UI_Label_labelMaximumFilterEntries { + public static string SettingsDialog_UI_Label_labelMaxDisplayLength { get { - return ResourceManager.GetString("SettingsDialog_UI_Label_labelMaximumFilterEntries", resourceCulture); + return ResourceManager.GetString("SettingsDialog_UI_Label_labelMaxDisplayLength", resourceCulture); } } /// - /// Looks up a localized string similar to Maximum Display Length (restart required). + /// Looks up a localized string similar to Max file age (days):. /// - public static string SettingsDialog_UI_Label_labelMaxDisplayLength { + public static string SettingsDialog_UI_Label_labelMaxFileAgeDays { get { - return ResourceManager.GetString("SettingsDialog_UI_Label_labelMaxDisplayLength", resourceCulture); + return ResourceManager.GetString("SettingsDialog_UI_Label_labelMaxFileAgeDays", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to 0 = Off. + /// + public static string SettingsDialog_UI_Label_labelMaxFileAgeDaysHint { + get { + return ResourceManager.GetString("SettingsDialog_UI_Label_labelMaxFileAgeDaysHint", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Maximum filter entries. + /// + public static string SettingsDialog_UI_Label_labelMaximumFilterEntries { + get { + return ResourceManager.GetString("SettingsDialog_UI_Label_labelMaximumFilterEntries", resourceCulture); } } @@ -6328,6 +6498,24 @@ public static string SettingsDialog_UI_Label_labelWorkingDir { } } + /// + /// Looks up a localized string similar to For $D patterns: how many consecutive days without a log file to skip before giving up.. + /// + public static string SettingsDialog_UI_NumericUpDown_ToolTip_upDownMultifileDays { + get { + return ResourceManager.GetString("SettingsDialog_UI_NumericUpDown_ToolTip_upDownMultifileDays", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Maximum age of rollover files to load, in days. Set to 0 to disable the age limit.. + /// + public static string SettingsDialog_UI_NumericUpDown_ToolTip_upDownMultifileMaxFileAgeDays { + get { + return ResourceManager.GetString("SettingsDialog_UI_NumericUpDown_ToolTip_upDownMultifileMaxFileAgeDays", resourceCulture); + } + } + /// /// Looks up a localized string similar to Failed to activate portable mode: {0}. /// @@ -6615,7 +6803,7 @@ public static string SettingsDialog_UI_TabPage_tabPageMarkerBar { return ResourceManager.GetString("SettingsDialog_UI_TabPage_tabPageMarkerBar", resourceCulture); } } - + /// /// Looks up a localized string similar to Memory/CPU. /// @@ -6810,131 +6998,5 @@ public static string ToolArgsDialog_UI_Title { return ResourceManager.GetString("ToolArgsDialog_UI_Title", resourceCulture); } } - - /// - /// Looks up a localized string similar to Create a new highlight item (opens the editor dialog). - /// - public static string HighlightDialog_UI_Button_ToolTip_btnAdd { - get { - return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnAdd", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Copy the selected highlight into a new one (opens the editor dialog). - /// - public static string HighlightDialog_UI_Button_ToolTip_btnCopy { - get { - return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnCopy", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Edit the selected highlight in the editor dialog. - /// - public static string HighlightDialog_UI_Button_ToolTip_btnEdit { - get { - return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnEdit", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Delete the current highlight. - /// - public static string HighlightDialog_UI_Button_ToolTip_btnDelete { - get { - return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnDelete", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Move the current highlight one position up. - /// - public static string HighlightDialog_UI_Button_ToolTip_btnMoveUp { - get { - return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnMoveUp", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Move the current highlight one position down. - /// - public static string HighlightDialog_UI_Button_ToolTip_btnMoveDown { - get { - return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnMoveDown", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Export highlight groups. - /// - public static string HighlightDialog_UI_Button_ToolTip_btnExportGroup { - get { - return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnExportGroup", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Import highlight groups. - /// - public static string HighlightDialog_UI_Button_ToolTip_btnImportGroup { - get { - return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnImportGroup", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Move the current highlight group one position down. - /// - public static string HighlightDialog_UI_Button_ToolTip_btnMoveGroupDown { - get { - return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnMoveGroupDown", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Move the current highlight group one position up. - /// - public static string HighlightDialog_UI_Button_ToolTip_btnMoveGroupUp { - get { - return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnMoveGroupUp", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Copy the current highlight group into a new one. - /// - public static string HighlightDialog_UI_Button_ToolTip_btnCopyGroup { - get { - return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnCopyGroup", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Delete the current highlight group. - /// - public static string HighlightDialog_UI_Button_ToolTip_btnDeleteGroup { - get { - return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnDeleteGroup", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Create a new empty highlight group. - /// - public static string HighlightDialog_UI_Button_ToolTip_btnNewGroup { - get { - return ResourceManager.GetString("HighlightDialog_UI_Button_ToolTip_btnNewGroup", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Choose a group to create different highlight settings. Type in a name to change the name of a group.. - /// - public static string HighlightDialog_UI_ComboBox_ToolTip_comboBoxGroups { - get { - return ResourceManager.GetString("HighlightDialog_UI_ComboBox_ToolTip_comboBoxGroups", resourceCulture); - } - } } } diff --git a/src/LogExpert.Resources/Resources.de.resx b/src/LogExpert.Resources/Resources.de.resx index 67ef82bf3..f204a5fc5 100644 --- a/src/LogExpert.Resources/Resources.de.resx +++ b/src/LogExpert.Resources/Resources.de.resx @@ -736,8 +736,20 @@ Damit die anderen Fenster beim Auswählen einer Zeile (Mausklick oder Pfeiltaste Pattern: - - Maximale Tage: + + Max. Tageslücke: + + + Max. Dateialter (Tage): + + + 0 = Aus + + + Bei $D-Mustern: Anzahl aufeinanderfolgender Tage ohne Protokolldatei, die vor dem Abbruch übersprungen werden. + + + Maximales Alter der zu ladenden Rollover-Dateien in Tagen. 0 deaktiviert die Altersgrenze. Argumente: @@ -1691,8 +1703,14 @@ Ein ausgewähltes Tool erscheint in der Iconbar. Alle anderen verfügbaren Tools Dateinamemuster: - - Max Tage: + + Max. Tageslücke: + + + Max. Dateialter (Tage): + + + 0 = Aus Muster syntax: @@ -1711,7 +1729,11 @@ Beispiele: DD = Tag MM = Monat YY[YY] = Jahr -Alle anderen Zeichen werden benutzt wie sie angegeben sind +Alle anderen Zeichen werden benutzt wie sie angegeben sind + +MultiFile-Grenzen: +Max. Tageslücke = Bei $D-Mustern aufeinanderfolgende Tage ohne Protokolldatei, die vor dem Abbruch übersprungen werden +Max. Dateialter (Tage) = Älteste zu ladende Rollover-Dateien in Tagen; 0 deaktiviert die Altersgrenze Spalten diff --git a/src/LogExpert.Resources/Resources.resx b/src/LogExpert.Resources/Resources.resx index bd9b3a535..65907ae72 100644 --- a/src/LogExpert.Resources/Resources.resx +++ b/src/LogExpert.Resources/Resources.resx @@ -722,8 +722,20 @@ Checked tools will appear in the icon bar. All other tools are available in the Arguments: - - Max days: + + Max day gap: + + + Max file age (days): + + + 0 = Off + + + For $D patterns: how many consecutive days without a log file to skip before giving up. + + + Maximum age of rollover files to load, in days. Set to 0 to disable the age limit. Pattern: @@ -1742,8 +1754,14 @@ Checked tools will appear in the icon bar. All other tools are available in the File name pattern: - - Max days: + + Max day gap: + + + Max file age (days): + + + 0 = Off Pattern syntax: @@ -1762,7 +1780,11 @@ Examples: DD = day MM = month YY[YY] = year -all other chars will be used as given +all other chars will be used as given + +MultiFile limits: +Max day gap = For $D patterns, consecutive days without a log file to skip before giving up +Max file age (days) = Oldest rollover files to load; 0 disables the age limit Columns diff --git a/src/LogExpert.Resources/Resources.zh-CN.resx b/src/LogExpert.Resources/Resources.zh-CN.resx index 9b875814b..b52b2490b 100644 --- a/src/LogExpert.Resources/Resources.zh-CN.resx +++ b/src/LogExpert.Resources/Resources.zh-CN.resx @@ -622,8 +622,20 @@ 参数: - - 最大天数: + + 最大日期间隔: + + + 最大文件年龄(天): + + + 0 = 关闭 + + + 对于 $D 模式:放弃搜索前可跳过的连续无日志文件天数。 + + + 要加载的滚动日志文件的最大天数。设置为 0 可关闭文件期限限制。 模式: @@ -1560,8 +1572,14 @@ 文件名模式: - - 最大天数: + + 最大日期间隔: + + + 最大文件年龄(天): + + + 0 = 关闭 模式语法: @@ -1580,7 +1598,11 @@ $J(<前缀>) = 类似 $J,但在非零时添加 <前缀> DD = 日 MM = 月 YY[YY] = 年 -其它字符将按原样使用 +其它字符将按原样使用 + +多文件限制: +最大日期间隔 = 对于 $D 模式,放弃搜索前可跳过的连续无日志文件天数 +最大文件年龄(天) = 要加载的最旧滚动日志文件天数;0 表示关闭期限限制 列 diff --git a/src/LogExpert.Tests/ConfigManagerTests/PreferencesMultiFileOptionsTests.cs b/src/LogExpert.Tests/ConfigManagerTests/PreferencesMultiFileOptionsTests.cs new file mode 100644 index 000000000..b076eb07c --- /dev/null +++ b/src/LogExpert.Tests/ConfigManagerTests/PreferencesMultiFileOptionsTests.cs @@ -0,0 +1,70 @@ +using System.Reflection; + +using LogExpert.Configuration; +using LogExpert.Core.Config; +using LogExpert.Core.Entities; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using NUnit.Framework; + +namespace LogExpert.Tests.ConfigManagerTests; + +[TestFixture] +public class PreferencesMultiFileOptionsTests +{ + [Test] + public void Deserialize_LegacyJsonWithoutMaxFileAgeDays_DefaultsToOff () + { + const string legacyJson = """ + { + "MultiFileOptions": { + "FormatPattern": "*$J(.)", + "MaxDayTry": 7 + } + } + """; + + var preferences = JsonConvert.DeserializeObject(legacyJson); + + Assert.That(preferences, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(preferences!.MultiFileOptions.MaxDayTry, Is.EqualTo(7)); + Assert.That(preferences.MultiFileOptions.MaxFileAgeDays, Is.Zero); + }); + } + + [Test] + public void RoundTrip_PreservesMaxFileAgeDays () + { + var preferences = new Preferences + { + MultiFileOptions = new MultiFileOptions { MaxFileAgeDays = 14 } + }; + + var restored = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(preferences)); + + Assert.That(restored, Is.Not.Null); + Assert.That(restored!.MultiFileOptions.MaxFileAgeDays, Is.EqualTo(14)); + } + + [Test] + public void Serialize_ContainsOnlyPersistedMultiFileOptions () + { + var preferences = new Preferences + { + MultiFileOptions = new MultiFileOptions { MaxFileAgeDays = 14 } + }; + + var settingsField = typeof(ConfigManager).GetField("_jsonSettings", BindingFlags.NonPublic | BindingFlags.Static); + Assert.That(settingsField, Is.Not.Null); + var serializerSettings = (JsonSerializerSettings)settingsField!.GetValue(null)!; + + var json = JsonConvert.SerializeObject(preferences.MultiFileOptions, serializerSettings); + var propertyNames = JObject.Parse(json).Properties().Select(property => property.Name); + + Assert.That(propertyNames, Is.EquivalentTo(["$id", "FormatPattern", "MaxDayTry", "MaxFileAgeDays"])); + } +} diff --git a/src/LogExpert.Tests/RolloverFilenameHandlerAgeTests.cs b/src/LogExpert.Tests/RolloverFilenameHandlerAgeTests.cs new file mode 100644 index 000000000..13b444378 --- /dev/null +++ b/src/LogExpert.Tests/RolloverFilenameHandlerAgeTests.cs @@ -0,0 +1,207 @@ +using ColumnizerLib; + +using LogExpert.Core.Classes.Log; +using LogExpert.Core.Entities; +using LogExpert.Core.Interfaces; + +using Moq; + +using NUnit.Framework; + +namespace LogExpert.Tests; + +[TestFixture] +internal sealed class RolloverFilenameHandlerAgeTests +{ + private const string LOG_DIRECTORY = "/logs"; + private static readonly DateTimeOffset _now = new(2026, 10, 19, 12, 0, 0, TimeSpan.Zero); + + [TestCase("*$J(.)", "app.log.1", "app.log.2")] + [TestCase("*$J(.).log", "app.1.log", "app.2.log")] + public void GetNameList_MaxFileAge_StopsAtFirstOldIndexedFile ( + string pattern, + string recentFileName, + string oldFileName) + { + var context = CreateContext( + "app.log", + new MultiFileOptions { FormatPattern = pattern, MaxFileAgeDays = 5 }, + (recentFileName, _now.AddDays(-2).UtcDateTime), + (oldFileName, _now.AddDays(-10).UtcDateTime)); + + var result = context.Handler.GetNameList(context.Registry); + + Assert.That(result, Is.EqualTo(new[] { GetPath(recentFileName), GetPath("app.log") })); + } + + [Test] + public void GetNameList_ZeroMaxFileAge_LoadsAllIndexedFiles () + { + var context = CreateContext( + "app.log", + new MultiFileOptions { FormatPattern = "*$J(.)", MaxFileAgeDays = 0 }, + ("app.log.1", _now.AddDays(-10).UtcDateTime), + ("app.log.2", _now.AddDays(-20).UtcDateTime)); + + var result = context.Handler.GetNameList(context.Registry); + + Assert.That(result, Is.EqualTo(new[] { GetPath("app.log.2"), GetPath("app.log.1"), GetPath("app.log") })); + } + + [Test] + public void GetNameList_FileAgeAboveMaximum_IsClampedWithoutOverflow () + { + var context = CreateContext( + "app.log", + new MultiFileOptions { FormatPattern = "*$J(.)", MaxFileAgeDays = int.MaxValue }, + ("app.log.1", _now.AddDays(-10).UtcDateTime)); + + var result = context.Handler.GetNameList(context.Registry); + + Assert.That(result, Is.EqualTo(new[] { GetPath("app.log.1"), GetPath("app.log") })); + } + + [Test] + public void GetNameList_FirstRolloverIsOld_StopsBeforeNewerHigherIndex () + { + var context = CreateContext( + "app.log", + new MultiFileOptions { FormatPattern = "*$J(.)", MaxFileAgeDays = 5 }, + ("app.log.1", _now.AddDays(-10).UtcDateTime), + ("app.log.2", _now.AddDays(-1).UtcDateTime)); + + var result = context.Handler.GetNameList(context.Registry); + + Assert.Multiple(() => + { + Assert.That(result, Is.EqualTo(new[] { GetPath("app.log") })); + Assert.That(context.RequestedPaths, Does.Not.Contain(GetPath("app.log.2"))); + }); + } + + [Test] + public void GetNameList_OpenedFileIsOld_StillIncludesOpenedFile () + { + var context = CreateContext( + "app.log", + new MultiFileOptions { FormatPattern = "*$J(.)", MaxFileAgeDays = 5 }, + activeLastWriteTimeUtc: _now.AddDays(-10).UtcDateTime); + + var result = context.Handler.GetNameList(context.Registry); + + Assert.That(result, Is.EqualTo(new[] { GetPath("app.log") })); + } + + [Test] + public void GetNameList_DatePattern_SkipsConfiguredGapAndStopsAtFirstOldFile () + { + var context = CreateContext( + "app_2026-10-19.log", + new MultiFileOptions + { + FormatPattern = "*$D(yyyy-MM-dd).log", + MaxDayTry = 3, + MaxFileAgeDays = 5 + }, + ("app_2026-10-17.log", _now.AddDays(-2).UtcDateTime), + ("app_2026-10-16.log", _now.AddDays(-10).UtcDateTime)); + + var result = context.Handler.GetNameList(context.Registry); + + Assert.That(result, Is.EqualTo(new[] { GetPath("app_2026-10-17.log"), GetPath("app_2026-10-19.log") })); + } + + [Test] + public void GetNameList_UnknownLastWriteTime_IncludesFile () + { + var context = CreateContext( + "app.log", + new MultiFileOptions { FormatPattern = "*$J(.)", MaxFileAgeDays = 5 }, + ("app.log.1", null)); + + var result = context.Handler.GetNameList(context.Registry); + + Assert.That(result, Is.EqualTo(new[] { GetPath("app.log.1"), GetPath("app.log") })); + } + + [Test] + public void GetNameList_MixedPattern_OldIndexedFileStopsBeforeDateFallback () + { + var previousDateFile = "app_2026-10-18.log"; + var context = CreateContext( + "app_2026-10-19.log", + new MultiFileOptions + { + FormatPattern = "*$D(yyyy-MM-dd)$J(.).log", + MaxDayTry = 3, + MaxFileAgeDays = 5 + }, + ("app_2026-10-19.1.log", _now.AddDays(-10).UtcDateTime), + (previousDateFile, _now.AddDays(-1).UtcDateTime)); + + var result = context.Handler.GetNameList(context.Registry); + + Assert.Multiple(() => + { + Assert.That(result, Is.EqualTo(new[] { GetPath("app_2026-10-19.log") })); + Assert.That(context.RequestedPaths, Does.Not.Contain(GetPath(previousDateFile))); + }); + } + + private static TestContext CreateContext ( + string activeFileName, + MultiFileOptions options, + params (string FileName, DateTime? LastWriteTimeUtc)[] rolloverFiles) + { + return CreateContext(activeFileName, options, _now.UtcDateTime, rolloverFiles); + } + + private static TestContext CreateContext ( + string activeFileName, + MultiFileOptions options, + DateTime? activeLastWriteTimeUtc, + params (string FileName, DateTime? LastWriteTimeUtc)[] rolloverFiles) + { + var activeFile = CreateFileInfo(activeFileName, activeLastWriteTimeUtc); + var files = rolloverFiles.ToDictionary(file => GetPath(file.FileName), + file => CreateFileInfo(file.FileName, file.LastWriteTimeUtc)); + List requestedPaths = []; + + var fileSystem = new Mock(); + _ = fileSystem.Setup(fs => fs.GetLogfileInfo(It.IsAny())) + .Returns((string path) => + { + requestedPaths.Add(path); + return files.GetValueOrDefault(path)?.Object; + }); + + var registry = new Mock(); + _ = registry.Setup(r => r.FindFileSystemForUri(It.IsAny())).Returns(fileSystem.Object); + + RolloverFilenameHandler handler = new(activeFile.Object, options, new FixedTimeProvider(_now)); + return new TestContext(handler, registry.Object, requestedPaths); + } + + private static Mock CreateFileInfo (string fileName, DateTime? lastWriteTimeUtc) + { + var fileInfo = new Mock(); + _ = fileInfo.SetupGet(file => file.FileName).Returns(fileName); + _ = fileInfo.SetupGet(file => file.DirectoryName).Returns(LOG_DIRECTORY); + _ = fileInfo.SetupGet(file => file.DirectorySeparatorChar).Returns('/'); + _ = fileInfo.SetupGet(file => file.FileExists).Returns(true); + _ = fileInfo.SetupGet(file => file.LastWriteTimeUtc).Returns(lastWriteTimeUtc); + return fileInfo; + } + + private static string GetPath (string fileName) => $"{LOG_DIRECTORY}/{fileName}"; + + private sealed record TestContext ( + RolloverFilenameHandler Handler, + IPluginRegistry Registry, + List RequestedPaths); + + private sealed class FixedTimeProvider (DateTimeOffset utcNow) : TimeProvider + { + public override DateTimeOffset GetUtcNow () => utcNow; + } +} diff --git a/src/LogExpert.UI.Tests/Controls/LogWindowCoordinatorIntegrationTests.cs b/src/LogExpert.UI.Tests/Controls/LogWindowCoordinatorIntegrationTests.cs index bae722bb1..3cdae3697 100644 --- a/src/LogExpert.UI.Tests/Controls/LogWindowCoordinatorIntegrationTests.cs +++ b/src/LogExpert.UI.Tests/Controls/LogWindowCoordinatorIntegrationTests.cs @@ -1,3 +1,4 @@ +using System.Reflection; using System.Runtime.Versioning; using LogExpert.Core.Config; @@ -115,6 +116,39 @@ public void Preferences_ReadsFromConfigManager_NotCoordinator () Assert.That(preferences, Is.SameAs(_settings.Preferences)); } + [Test] + public void SetDefaultsFromPrefs_SeedsNewWindowWithGlobalMultiFileOptions () + { + _settings.Preferences.MultiFileOptions = new MultiFileOptions + { + FormatPattern = "*$J(.).log", + MaxDayTry = 4, + MaxFileAgeDays = 30 + }; + var setDefaults = typeof(LogWindow).GetMethod("SetDefaultsFromPrefs", + BindingFlags.Instance | BindingFlags.NonPublic); + var optionsField = typeof(LogWindow).GetField("_multiFileOptions", + BindingFlags.Instance | BindingFlags.NonPublic); + + Assert.Multiple(() => + { + Assert.That(setDefaults, Is.Not.Null); + Assert.That(optionsField, Is.Not.Null); + }); + + _ = _logWindow.Handle; + _ = setDefaults!.Invoke(_logWindow, null); + var windowOptions = (MultiFileOptions)optionsField!.GetValue(_logWindow)!; + + Assert.Multiple(() => + { + Assert.That(windowOptions, Is.Not.SameAs(_settings.Preferences.MultiFileOptions)); + Assert.That(windowOptions.FormatPattern, Is.EqualTo("*$J(.).log")); + Assert.That(windowOptions.MaxDayTry, Is.EqualTo(4)); + Assert.That(windowOptions.MaxFileAgeDays, Is.EqualTo(30)); + }); + } + [Test] public void SearchParams_ReadsFromCoordinator () { diff --git a/src/LogExpert.UI.Tests/Dialogs/MultiFileAgeSettingsTests.cs b/src/LogExpert.UI.Tests/Dialogs/MultiFileAgeSettingsTests.cs new file mode 100644 index 000000000..dfee68483 --- /dev/null +++ b/src/LogExpert.UI.Tests/Dialogs/MultiFileAgeSettingsTests.cs @@ -0,0 +1,135 @@ +using System.Reflection; + +using LogExpert.Core.Config; +using LogExpert.Core.Entities; +using LogExpert.Core.Interfaces; +using LogExpert.Dialogs; +using LogExpert.UI.Dialogs; + +using Moq; + +using NUnit.Framework; + +using UIStrings = LogExpert.Resources; + +namespace LogExpert.UI.Tests.Dialogs; + +[TestFixture] +[Apartment(ApartmentState.STA)] +public class MultiFileAgeSettingsTests +{ + [Test] + public void MultiFileMaskDialog_LoadsAndSavesBothLimits () + { + using var dialog = new MultiFileMaskDialog("app.log") + { + FileNamePattern = "*$J(.)", + MaxDayGap = 3, + MaxFileAgeDays = 14 + }; + + InvokePrivate(dialog, "OnMultiFileMaskDialogLoad"); + + var maxDayGap = Find(dialog, "upDownMaxDayGap"); + var maxFileAgeDays = Find(dialog, "upDownMaxFileAgeDays"); + Assert.Multiple(() => + { + Assert.That(maxDayGap.Value, Is.EqualTo(3)); + Assert.That(maxFileAgeDays.Value, Is.EqualTo(14)); + Assert.That(maxFileAgeDays.Maximum, Is.EqualTo(MultiFileOptions.MAX_FILE_AGE_DAYS)); + Assert.That(Find