diff --git a/src/LogExpert.Tests/LogExpert.Tests.csproj b/src/LogExpert.Tests/LogExpert.Tests.csproj index f35a6ca8..271b8f26 100644 --- a/src/LogExpert.Tests/LogExpert.Tests.csproj +++ b/src/LogExpert.Tests/LogExpert.Tests.csproj @@ -33,13 +33,13 @@ - + PreserveNewest - + PreserveNewest - + PreserveNewest diff --git a/src/LogExpert.Tests/Services/SessionHandlerTests.cs b/src/LogExpert.Tests/Services/SessionHandlerTests.cs index 5153cb43..d2c30616 100644 --- a/src/LogExpert.Tests/Services/SessionHandlerTests.cs +++ b/src/LogExpert.Tests/Services/SessionHandlerTests.cs @@ -21,6 +21,8 @@ internal class SessionHandlerTests : IDisposable { private Mock _pluginRegistryMock = null!; private List _addFileTabCalls = null!; + private List _callOrder = null!; + private int _closeAllTabsCallCount; private Settings _settings; private Mock _configManagerMock; private LogWindow _stubLogWindow = null!; @@ -38,6 +40,8 @@ public void SetUp () _pluginRegistryMock = new Mock(); _configManagerMock = new Mock(); _addFileTabCalls = []; + _callOrder = []; + _closeAllTabsCallCount = 0; _settings = new Settings(); _ = _configManagerMock.Setup(cm => cm.Settings).Returns(_settings); @@ -59,7 +63,13 @@ public void SetUp () request => { _addFileTabCalls.Add(request); + _callOrder.Add($"addtab:{request.FileName}"); return _stubLogWindow; + }, + () => + { + _closeAllTabsCallCount++; + _callOrder.Add("close"); }); } @@ -364,9 +374,8 @@ public void ContinueLoad_DuplicateFileWithAlternative_ReplacesBothOccurrences () } [Test] - public void ContinueLoad_CloseAllTabs_ReturnsTrueInResult () + public void ContinueLoad_CloseAllTabs_ClosesExistingTabsBeforeOpeningNewWindows () { - // Arrange var outcome = CreateOutcome( SessionLoadOutcome.LoadStatus.NeedsIntervention, ["C:\\logs\\app.log"], @@ -378,7 +387,9 @@ public void ContinueLoad_CloseAllTabs_ReturnsTrueInResult () var result = _sut.ContinueLoad(outcome, resolution, restoreLayout: true); // Assert - Assert.That(result.CloseAllTabs, Is.True); + Assert.That(_closeAllTabsCallCount, Is.EqualTo(1), "close must be invoked exactly once"); + Assert.That(_callOrder, Is.EqualTo(new[] { "close", "addtab:C:\\logs\\app.log" }), + "existing tabs must be closed before the new windows are opened"); Assert.That(result.OpenedTabs, Is.True); Assert.That(result.OpenInNewWindowFiles, Is.Null); } @@ -402,6 +413,7 @@ public void ContinueLoad_OpenInNewWindow_DoesNotOpenTabs_ReturnsFileNames () Assert.That(result.OpenInNewWindowFiles, Is.Not.Null); Assert.That(result.OpenInNewWindowFiles, Has.Length.EqualTo(1)); Assert.That(_addFileTabCalls, Is.Empty, "OpenInNewWindow must not open tabs"); + Assert.That(_closeAllTabsCallCount, Is.EqualTo(0), "OpenInNewWindow must not close existing tabs"); } [Test] @@ -519,7 +531,7 @@ public void ContinueLoad_UpdateSessionFileFalse_DoesNotSave () } [Test] - public void ContinueLoad_NullResolution_SuccessReturnsCloseAllTabsFalse () + public void ContinueLoad_NullResolution_DoesNotCloseExistingTabs () { // Arrange var outcome = CreateSuccessOutcome(["C:\\logs\\app.log"], layoutXml: null); @@ -528,10 +540,29 @@ public void ContinueLoad_NullResolution_SuccessReturnsCloseAllTabsFalse () var result = _sut.ContinueLoad(outcome, resolution: null, restoreLayout: true); // Assert - Assert.That(result.CloseAllTabs, Is.False); + Assert.That(_closeAllTabsCallCount, Is.EqualTo(0), "existing tabs must not be closed when no resolution is given"); Assert.That(result.OpenInNewWindowFiles, Is.Null); } + [Test] + public void ContinueLoad_CloseAllTabsFalse_DoesNotCloseExistingTabs () + { + // Arrange + var outcome = CreateOutcome( + SessionLoadOutcome.LoadStatus.NeedsIntervention, + ["C:\\logs\\app.log"], + layoutXml: ""); + + var resolution = new MissingFilesResolution { CloseAllTabs = false }; + + // Act + _ = _sut.ContinueLoad(outcome, resolution, restoreLayout: true); + + // Assert + Assert.That(_closeAllTabsCallCount, Is.EqualTo(0)); + Assert.That(_addFileTabCalls, Has.Count.EqualTo(1)); + } + #endregion #region SaveSession tests diff --git a/src/LogExpert.Tests/Data/50 MB UTF16.txt b/src/LogExpert.Tests/TestData/50 MB UTF16.txt similarity index 100% rename from src/LogExpert.Tests/Data/50 MB UTF16.txt rename to src/LogExpert.Tests/TestData/50 MB UTF16.txt diff --git a/src/LogExpert.Tests/Data/50 MB UTF8.txt b/src/LogExpert.Tests/TestData/50 MB UTF8.txt similarity index 100% rename from src/LogExpert.Tests/Data/50 MB UTF8.txt rename to src/LogExpert.Tests/TestData/50 MB UTF8.txt diff --git a/src/LogExpert.Tests/Data/50 MB.txt b/src/LogExpert.Tests/TestData/50 MB.txt similarity index 100% rename from src/LogExpert.Tests/Data/50 MB.txt rename to src/LogExpert.Tests/TestData/50 MB.txt diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs index 30de7a99..7fa3260d 100644 --- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs +++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs @@ -8,7 +8,6 @@ using ColumnizerLib; using LogExpert.Core.Classes; -using LogExpert.Core.Classes.Columnizer; using LogExpert.Core.Classes.Persister; using LogExpert.Core.Config; using LogExpert.Core.Entities; @@ -115,7 +114,7 @@ public LogTabWindow (string[] fileNames, int instanceNumber, bool showInstanceNu _fileOperationService.FileHistoryChanged += (_, _) => FillHistoryMenu(); _fileOperationService.FileOpened += OnFileOperationServiceFileOpened; - _sessionHandler = new SessionHandler(PluginRegistry.PluginRegistry.Instance, request => _fileOperationService.AddFileTab(request)); + _sessionHandler = new SessionHandler(PluginRegistry.PluginRegistry.Instance, request => _fileOperationService.AddFileTab(request), CloseAllTabs); _toolLaunchService = new ToolLaunchService(PluginRegistry.PluginRegistry.Instance); _logWindowCoordinator = new LogWindowCoordinator(configManager, PluginRegistry.PluginRegistry.Instance, this, _tabController, _ledService, _fileOperationService); @@ -1460,6 +1459,8 @@ private void LoadSession (string sessionFileName, bool restoreLayout) SelectedAlternatives = selectedAlternatives }; + // ContinueLoad closes existing tabs (when requested) before opening the + // new session windows, so no post-hoc CloseAllTabs() is needed here. var interventionResult = _sessionHandler.ContinueLoad(outcome, resolution, restoreLayout); if (updateSessionFile) @@ -1469,11 +1470,6 @@ private void LoadSession (string sessionFileName, bool restoreLayout) MessageBoxIcon.Information); } - if (interventionResult.CloseAllTabs) - { - CloseAllTabs(); - } - if (interventionResult.OpenInNewWindowFiles is not null) { LogExpertProxy.NewWindow([.. interventionResult.OpenInNewWindowFiles]); diff --git a/src/LogExpert.UI/Services/SessionHandlerService/ContinueLoadResult.cs b/src/LogExpert.UI/Services/SessionHandlerService/ContinueLoadResult.cs index 250eaa02..c3ff3c8f 100644 --- a/src/LogExpert.UI/Services/SessionHandlerService/ContinueLoadResult.cs +++ b/src/LogExpert.UI/Services/SessionHandlerService/ContinueLoadResult.cs @@ -4,7 +4,5 @@ internal readonly record struct ContinueLoadResult { public bool OpenedTabs { get; init; } - public bool CloseAllTabs { get; init; } - public string[]? OpenInNewWindowFiles { get; init; } } \ No newline at end of file diff --git a/src/LogExpert.UI/Services/SessionHandlerService/SessionHandler.cs b/src/LogExpert.UI/Services/SessionHandlerService/SessionHandler.cs index 2178b37c..2185a601 100644 --- a/src/LogExpert.UI/Services/SessionHandlerService/SessionHandler.cs +++ b/src/LogExpert.UI/Services/SessionHandlerService/SessionHandler.cs @@ -13,7 +13,8 @@ namespace LogExpert.UI.Services.SessionHandlerService; [SupportedOSPlatform("windows")] internal sealed class SessionHandler ( IPluginRegistry pluginRegistry, - Func addFileTab) + Func addFileTab, + Action closeAllTabs) : ISessionHandler { private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); @@ -137,11 +138,19 @@ InvalidOperationException or return new ContinueLoadResult { OpenedTabs = false, - CloseAllTabs = false, OpenInNewWindowFiles = [.. resolvedFiles] }; } + // Close existing tabs BEFORE opening the new session windows. The new windows are + // tracked by the tab controller as soon as they are created (even deferred ones that + // are not yet on the dock panel), so closing afterwards would also close them and the + // subsequent layout restore would find nothing to dock. + if (resolution is { CloseAllTabs: true }) + { + closeAllTabs(); + } + // Open file tabs bool deferForLayout = loadOutcome.HasLayoutData && restoreLayout; int openedCount = 0; @@ -176,7 +185,6 @@ and not InvalidProgramException return new ContinueLoadResult { OpenedTabs = openedCount > 0, - CloseAllTabs = resolution?.CloseAllTabs ?? false, OpenInNewWindowFiles = null }; } diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs index 07228d8c..15809db5 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: 2026-06-15 07:15:17 UTC + /// Generated: 2026-06-17 13:20:19 UTC /// Configuration: Release /// Plugin count: 21 /// @@ -18,27 +18,27 @@ public static Dictionary GetBuiltInPluginHashes() { return new Dictionary(StringComparer.OrdinalIgnoreCase) { - ["AutoColumnizer.dll"] = "4180178F5A56D7F66E8A975FF7F780C7A7812F08160A892A3CE26E4ED1EFD0DC", + ["AutoColumnizer.dll"] = "61E445A9F8960311A1EF0A21001B121DCAFFFDD2243ACE04BCF840560B372EA5", ["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", ["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", - ["CsvColumnizer.dll"] = "703EF513A4DC02B4B69707DE237025AF4A87B0506A850D5231BCCB8CD6322ED1", - ["CsvColumnizer.dll (x86)"] = "703EF513A4DC02B4B69707DE237025AF4A87B0506A850D5231BCCB8CD6322ED1", - ["DefaultPlugins.dll"] = "339862337AACE8645B70F6E75F5FA4E875A6164FF055E9E8171E8709A650BA90", - ["FlashIconHighlighter.dll"] = "1B1697E0EB2416F656BD6120A2C64EBF06260F8B1346CDB851979B23AF8F8444", - ["GlassfishColumnizer.dll"] = "327AC39EEA2E04F055411644AED8D8603F760AF76BF9913E8ED4FA0211E84D73", - ["JsonColumnizer.dll"] = "8EA699F6C34300A43228375ED6590C682299C5577E164DF186019785A7F37F79", - ["JsonCompactColumnizer.dll"] = "174FCD6677405DC65AC3823D12042F9EFCB456B55F5457572A10C2E48A9D0F81", - ["Log4jXmlColumnizer.dll"] = "0AB084A6EF69717EF9DE7294D68F6F5D149815AA5828FDA93AEB1FD88EA9D7A1", - ["LogExpert.Resources.dll"] = "CF23AE53827FCDA8430ECF6E20F48B03D5BA86975F25747D5E4B380A946584A1", + ["CsvColumnizer.dll"] = "9DE6D6CA21188746BAE404F5EB5D4B3B0897A50ACD596994FE730033DE18DEBA", + ["CsvColumnizer.dll (x86)"] = "9DE6D6CA21188746BAE404F5EB5D4B3B0897A50ACD596994FE730033DE18DEBA", + ["DefaultPlugins.dll"] = "24DFE5E3B38360ADE7EB0C23DEB15AC98F2649CDC517C1E5A8E3D5666DDC1AB1", + ["FlashIconHighlighter.dll"] = "2F196A6B52D1ED5FAC921BC487E27A1DBA444667A76E55C912C145D49CA9CE85", + ["GlassfishColumnizer.dll"] = "EFAE70B8767B0C23F0EB72495968F4BEC6E0C1CE0E55484C15DA9DAFE97A0B74", + ["JsonColumnizer.dll"] = "5C5D99331694A0543750FD78B35CA3FF082949A45AC731FB06DE8B505F6CA2F5", + ["JsonCompactColumnizer.dll"] = "05386F718D6ECBAD1C788B55E70DC8888B8F8C7069702841B081B6CA9D7A54AC", + ["Log4jXmlColumnizer.dll"] = "24D85F8884DED3345E25A6592B4808771A6A77B005EF2EAAA05FB76756C36D8B", + ["LogExpert.Resources.dll"] = "F94CC3F814A338588EE3E92E4B4D559EDF7E2CF7554A6CDD95C1E0BA6FBCA613", ["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"] = "E67FCF12B4563F1C10E46571815D3AF952D9BBD9DA46215812A7FD31D753C8F5", - ["SftpFileSystem.dll"] = "014CD6FC3EF2A405033C0AB96D90CAB54B0229332F4DCF8176E2F43C1528F487", - ["SftpFileSystem.dll (x86)"] = "E23DDE1464E95D4621688655D9ABD761F0CCB221A5962B97D489E2B5A20BB87F", - ["SftpFileSystem.Resources.dll"] = "0155F98A60C39252C0006A1244A8BFD987DAC03406A3B3BF5D1E012C52FF2A48", - ["SftpFileSystem.Resources.dll (x86)"] = "0155F98A60C39252C0006A1244A8BFD987DAC03406A3B3BF5D1E012C52FF2A48", + ["RegexColumnizer.dll"] = "1FD933319BA6978B1E78444472BE7540EC76576327BB09C7900BE66320CAC2EF", + ["SftpFileSystem.dll"] = "49D9DB34840774486F3944B49B165279E68A8F8A43C0E93E5DE6D41F9317C1D2", + ["SftpFileSystem.dll (x86)"] = "EEAA1B42AE124ECDF0FF63A80C00455FB63778588B656822D366B4D4F5180CC7", + ["SftpFileSystem.Resources.dll"] = "6FFC5A1EECA8363E97F7235DD70EE2E9F91870C0BD45E9A3A17669D7BD38423D", + ["SftpFileSystem.Resources.dll (x86)"] = "6FFC5A1EECA8363E97F7235DD70EE2E9F91870C0BD45E9A3A17669D7BD38423D", }; }