Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
<LogExpertDocsDirectory>$(LogExpertBinRoot)Docs\</LogExpertDocsDirectory>

<SignAssembly>true</SignAssembly>
<!-- Hang timeout for every test run (dotnet test, Nuke, CI); see tests.runsettings. -->
<RunSettingsFilePath>$(MSBuildThisFileDirectory)tests.runsettings</RunSettingsFilePath>
<PackageProjectUrl>https://github.com/LogExperts/LogExpert</PackageProjectUrl>
<RepositoryUrl>https://github.com/LogExperts/LogExpert</RepositoryUrl>
<PackageTags>LogExpert, Columnizer, Logging, Windows, Winforms</PackageTags>
Expand Down
3 changes: 2 additions & 1 deletion src/LogExpert.Configuration/ConfigManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ public Settings Settings
/// </summary>
public string PortableSessionDir => Path.Join(PortableConfigDir, "sessions");

public string ConfigDir => Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "LogExpert");
/// <summary>%APPDATA%/LogExpert/; tests redirect it so they never touch the user's real configuration.</summary>
public string ConfigDir { get; internal set; } = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "LogExpert");

/// <summary>
/// Application.StartUpPath + portable
Expand Down
4 changes: 4 additions & 0 deletions src/LogExpert.Configuration/LogExpert.Configuration.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
<RootNamespace>LogExpert.Configuration</RootNamespace>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="LogExpert.Tests" Key="0024000004800000940000000602000000240000525341310004000001000100619e9beea345a3bb5e15f55b29ddf40d96e9bb473ae58304fc63dfb3e9c94d8944bb7e45324ee0bef3e345dccba79b0bf64b85a128a7f261861899add639218ddaeb2acc6fcc746d6acb5bb212d375a0967756af192cfdb6cf0bff666a0fe535600abda860d3eafaff4ef1c9b5710181f72d996ca9c29ed64bae4a5fd916dea5" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\LogExpert.Core\LogExpert.Core.csproj" />
</ItemGroup>
Expand Down
24 changes: 24 additions & 0 deletions src/LogExpert.Persister.Tests/PluginTrustIsolationSetup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using LogExpert.PluginRegistry;

using NUnit.Framework;

// Outside any namespace so it runs once before every fixture in the assembly.
[SetUpFixture]
public class PluginTrustIsolationSetup
{
private readonly string _configDir = Path.Join(Path.GetTempPath(), "LogExpert_TestPluginTrust_" + Guid.NewGuid().ToString("N"));

// Until initialized, PluginValidator reads and writes trusted-plugins.json in the developer's %APPDATA%\LogExpert.
[OneTimeSetUp]
public void RedirectPluginTrust ()
{
_ = Directory.CreateDirectory(_configDir);
PluginValidator.Initialize(_configDir);
}

[OneTimeTearDown]
public void RemovePluginTrustDir ()
{
Directory.Delete(_configDir, recursive: true);
}
}
29 changes: 29 additions & 0 deletions src/LogExpert.Tests/ConfigDirIsolationSetup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using LogExpert.Configuration;
using LogExpert.PluginRegistry;

using NUnit.Framework;

// Outside any namespace so it runs once before every fixture in the assembly.
[SetUpFixture]
public class ConfigDirIsolationSetup
{
private string _configDir;

[OneTimeSetUp]
public void RedirectConfigDir ()
{
_configDir = Path.Join(Path.GetTempPath(), "LogExpert_TestConfig_" + Guid.NewGuid().ToString("N"));
_ = Directory.CreateDirectory(_configDir);
ConfigManager.Instance.ConfigDir = _configDir;
PluginValidator.Initialize(_configDir);
}

[OneTimeTearDown]
public void RemoveConfigDir ()
{
if (Directory.Exists(_configDir))
{
Directory.Delete(_configDir, recursive: true);
}
}
}
23 changes: 23 additions & 0 deletions src/LogExpert.Tests/ConfigManagerTests/ConfigDirIsolationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Reflection;

using LogExpert.Configuration;
using LogExpert.PluginRegistry;

using NUnit.Framework;

namespace LogExpert.Tests.ConfigManagerTests;

[TestFixture]
public class ConfigDirIsolationTests
{
[Test]
public void Tests_NeverUseTheUsersRealConfigOrPluginTrustDirectory ()
{
var realConfigDir = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "LogExpert");

Assert.That(ConfigManager.Instance.ConfigDir, Is.Not.EqualTo(realConfigDir));

var trustFile = (string)typeof(PluginValidator).GetField("_configPath", BindingFlags.NonPublic | BindingFlags.Static)!.GetValue(null)!;
Assert.That(trustFile, Does.Not.StartWith(realConfigDir));
}
}
3 changes: 3 additions & 0 deletions src/LogExpert.UI.Tests/CommandLine/LineNavigationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ internal sealed class LineNavigationTests : IDisposable
private Mock<IConfigManager> _config;
private Settings _settings;
private LogTabWindow? _window;
private WinFormsSynchronizationScope? _synchronization;

[SetUp]
public void SetUp ()
{
_synchronization = new WinFormsSynchronizationScope();
_directory = Path.Join(Path.GetTempPath(), "LogExpertLineTests", Guid.NewGuid().ToString("N"));
_ = Directory.CreateDirectory(_directory);
_fileName = Path.Join(_directory, "application.log");
Expand Down Expand Up @@ -63,6 +65,7 @@ public void TearDown ()
_window?.Close();
_window?.Dispose();
_window = null;
_synchronization?.Dispose();
Directory.Delete(_directory, true);
}

Expand Down
3 changes: 3 additions & 0 deletions src/LogExpert.UI.Tests/HiddenLinesWindowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,14 @@ public sealed class HiddenLinesWindowTests : IDisposable
private Mock<IConfigManager> _config = null!;
private LogTabWindow? _window;
private Exception? _uiException;
private WinFormsSynchronizationScope? _synchronization;
Comment thread
Hirogen marked this conversation as resolved.
Dismissed

private HighlightEntry HideRule => _settings.Preferences.HighlightGroupList[0].HighlightEntryList[0];

[SetUp]
public void SetUp ()
{
_synchronization = new WinFormsSynchronizationScope();
_uiException = null;
_directory = Path.Join(Path.GetTempPath(), "LogExpertHiddenLinesTests", Guid.NewGuid().ToString("N"));
_ = Directory.CreateDirectory(_directory);
Expand Down Expand Up @@ -88,6 +90,7 @@ public void TearDown ()
finally
{
Application.ThreadException -= OnUiException;
_synchronization?.Dispose();
}

Directory.Delete(_directory, true);
Expand Down
4 changes: 4 additions & 0 deletions src/LogExpert.UI.Tests/MarkerWindowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ public sealed class MarkerWindowTests : IDisposable
private LogTabWindow? _window;
private Exception? _uiException;
private SystemColorMode _originalColorMode;
private WinFormsSynchronizationScope? _synchronization;
Comment thread
Hirogen marked this conversation as resolved.
Dismissed

[SetUp]
public void SetUp ()
{
_synchronization = new WinFormsSynchronizationScope();
_originalColorMode = Application.ColorMode;
_uiException = null;
_directory = Path.Join(Path.GetTempPath(), "LogExpertMarkerTests", Guid.NewGuid().ToString("N"));
Expand Down Expand Up @@ -94,6 +96,8 @@ public void TearDown ()
{
Application.SetColorMode(_originalColorMode);
}

_synchronization?.Dispose();
}

Directory.Delete(_directory, true);
Expand Down
19 changes: 19 additions & 0 deletions src/LogExpert.UI.Tests/Services/MenuToolbarControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,25 @@ public void TearDown ()
_syncContext?.Dispose();
}

[Test]
public void UpdateGuiState_OnTheUiThreadUnderAnotherContext_AppliesImmediately ()
{
// Deciding by context instance re-posted on the UI thread itself, looping forever and freezing it.
var previous = SynchronizationContext.Current;
try
{
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());

_controller.UpdateGuiState(new GuiStateEventArgs { FollowTail = true, MenuEnabled = true }, false);

Assert.That(_followTailCheckBox.Checked, Is.True);
}
finally
{
SynchronizationContext.SetSynchronizationContext(previous);
}
}

[Test]
public void UpdateGuiState_SetsFollowTailChecked ()
{
Expand Down
18 changes: 18 additions & 0 deletions src/LogExpert.UI.Tests/UiTestSetup.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Runtime.Versioning;

using LogExpert.PluginRegistry;

using NUnit.Framework;

namespace LogExpert.UI.Tests;
Expand All @@ -8,11 +10,27 @@ namespace LogExpert.UI.Tests;
[SupportedOSPlatform("windows")]
public class UiTestSetup
{
private readonly string _pluginTrustDir = Path.Join(Path.GetTempPath(), "LogExpert_UiTestPluginTrust_" + Guid.NewGuid().ToString("N"));

[OneTimeSetUp]
public void EnableApplicationDpiMode ()
{
// Match LogExpert.csproj before any fixture creates a window handle.
_ = Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);
Assert.That(Application.HighDpiMode, Is.EqualTo(HighDpiMode.PerMonitorV2));

// On by default only under a debugger; off, a control touched from a worker thread can create its
// window there and later deadlock the UI thread instead of failing the test.
Control.CheckForIllegalCrossThreadCalls = true;

// Until initialized, PluginValidator reads and writes trusted-plugins.json in the developer's %APPDATA%\LogExpert.
_ = Directory.CreateDirectory(_pluginTrustDir);
PluginValidator.Initialize(_pluginTrustDir);
}

[OneTimeTearDown]
public void RemovePluginTrustDir ()
{
Directory.Delete(_pluginTrustDir, recursive: true);
}
}
23 changes: 23 additions & 0 deletions src/LogExpert.UI.Tests/WinFormsSynchronizationScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace LogExpert.UI.Tests;

/// <summary>
/// Installs the WinForms synchronization context the app runs with. NUnit gives STA tests its own context, which
/// WinForms does not replace, so services that capture it would run UI callbacks on other threads.
/// </summary>
internal sealed class WinFormsSynchronizationScope : IDisposable
{
private readonly SynchronizationContext? _previous = SynchronizationContext.Current;
private readonly WindowsFormsSynchronizationContext _context = new();

public WinFormsSynchronizationScope ()
{
SynchronizationContext.SetSynchronizationContext(_context);
}

// Not disposed: disposing a WinForms context destroys the thread's shared marshaling control, and await
// continuations still queued by the test would then crash the test host.
public void Dispose ()
{
SynchronizationContext.SetSynchronizationContext(_previous);
}
}
3 changes: 2 additions & 1 deletion src/LogExpert.UI/Services/LedService/LedIndicatorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ internal sealed class LedIndicatorService : ILedIndicatorService, IDisposable
// Animation
private System.Windows.Forms.Timer _animationTimer;
private readonly SynchronizationContext _uiContext;
private readonly int _uiThreadId = Environment.CurrentManagedThreadId;
private bool _isInitialized;
private bool _disposed;

Expand Down Expand Up @@ -664,7 +665,7 @@ private void OnIconChanged (LogWindow window, Icon icon)
var args = new IconChangedEventArgs(window, icon);

// Marshal to UI thread if needed
if (SynchronizationContext.Current != _uiContext)
if (Environment.CurrentManagedThreadId != _uiThreadId)
{
_uiContext.Post(_ => IconChanged?.Invoke(this, args), null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ internal sealed class MenuToolbarController : IMenuToolbarController
private ToolStripMenuItem _lastUsedMenuItem;

private readonly SynchronizationContext _uiContext;
private readonly int _uiThreadId = Environment.CurrentManagedThreadId;
private bool _disposed;
private bool _suppressEvents;

Expand Down Expand Up @@ -111,7 +112,7 @@ public void UpdateGuiState (GuiStateEventArgs state, bool timestampControlEnable
{
ArgumentNullException.ThrowIfNull(state);

if (_uiContext != SynchronizationContext.Current)
if (Environment.CurrentManagedThreadId != _uiThreadId)
{
_uiContext.Post(_ => UpdateGuiState(state, timestampControlEnabled), null);
return;
Expand Down Expand Up @@ -185,7 +186,7 @@ public void UpdateGuiState (GuiStateEventArgs state, bool timestampControlEnable

public void UpdateEncodingMenu (Encoding currentEncoding)
{
if (_uiContext != SynchronizationContext.Current)
if (Environment.CurrentManagedThreadId != _uiThreadId)
{
_uiContext.Post(_ => UpdateEncodingMenu(currentEncoding), null);
return;
Expand Down Expand Up @@ -220,7 +221,7 @@ public void UpdateHighlightGroups (IEnumerable<string> groups, string selectedGr
return;
}

if (_uiContext != SynchronizationContext.Current)
if (Environment.CurrentManagedThreadId != _uiThreadId)
{
_uiContext.Post(_ => UpdateHighlightGroups(groups, selectedGroup), null);
return;
Expand Down Expand Up @@ -254,7 +255,7 @@ public void PopulateFileHistory (IEnumerable<string> fileHistory)
return;
}

if (_uiContext != SynchronizationContext.Current)
if (Environment.CurrentManagedThreadId != _uiThreadId)
{
_uiContext.Post(_ => PopulateFileHistory(fileHistory), null);
return;
Expand Down
24 changes: 24 additions & 0 deletions src/PluginRegistry.Tests/PluginTrustIsolationSetup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using LogExpert.PluginRegistry;

using NUnit.Framework;

// Outside any namespace so it runs once before every fixture in the assembly.
[SetUpFixture]
public class PluginTrustIsolationSetup
{
private readonly string _configDir = Path.Join(Path.GetTempPath(), "LogExpert_TestPluginTrust_" + Guid.NewGuid().ToString("N"));

// Until initialized, PluginValidator reads and writes trusted-plugins.json in the developer's %APPDATA%\LogExpert.
[OneTimeSetUp]
public void RedirectPluginTrust ()
{
_ = Directory.CreateDirectory(_configDir);
PluginValidator.Initialize(_configDir);
}

[OneTimeTearDown]
public void RemovePluginTrustDir ()
{
Directory.Delete(_configDir, recursive: true);
}
}
15 changes: 15 additions & 0 deletions src/tests.runsettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Applied to every test project through RunSettingsFilePath in Directory.Build.props. -->
<RunSettings>
<DataCollectionRunSettings>
<DataCollectors>
<!-- A single hung test (e.g. a WinForms deadlock) kills the test host after 3 minutes with a mini dump,
and the run reports which test was running, instead of the whole run freezing. -->
<DataCollector friendlyName="blame" enabled="true">
<Configuration>
<CollectDumpOnTestSessionHang TestTimeout="3m" HangDumpType="mini" />
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
</RunSettings>
Loading