diff --git a/EventLogExpert.slnx b/EventLogExpert.slnx
index 67aabbb4..c9c24a96 100644
--- a/EventLogExpert.slnx
+++ b/EventLogExpert.slnx
@@ -23,6 +23,11 @@
+
+
+
+
+
diff --git a/src/EventLogExpert.EventDbTool/CreateDatabaseCommand.cs b/src/EventLogExpert.EventDbTool/CreateDatabaseCommand.cs
index 95862cee..39fd6561 100644
--- a/src/EventLogExpert.EventDbTool/CreateDatabaseCommand.cs
+++ b/src/EventLogExpert.EventDbTool/CreateDatabaseCommand.cs
@@ -70,7 +70,7 @@ public static Command GetCommand()
return createDatabaseCommand;
}
- private void CreateDatabase(string path, string? source, string? filter, string? skipProvidersInFile)
+ internal void CreateDatabase(string path, string? source, string? filter, string? skipProvidersInFile)
{
if (File.Exists(path))
{
diff --git a/src/EventLogExpert.EventDbTool/Properties/AssemblyInfo.cs b/src/EventLogExpert.EventDbTool/Properties/AssemblyInfo.cs
index 6e18265f..fa5fabc5 100644
--- a/src/EventLogExpert.EventDbTool/Properties/AssemblyInfo.cs
+++ b/src/EventLogExpert.EventDbTool/Properties/AssemblyInfo.cs
@@ -3,4 +3,5 @@
using System.Runtime.CompilerServices;
+[assembly: InternalsVisibleTo("EventLogExpert.EventDbTool.IntegrationTests")]
[assembly: InternalsVisibleTo("EventLogExpert.EventDbTool.Tests")]
diff --git a/src/EventLogExpert.EventDbTool/ShowCommand.cs b/src/EventLogExpert.EventDbTool/ShowCommand.cs
index 489bdb1d..95373655 100644
--- a/src/EventLogExpert.EventDbTool/ShowCommand.cs
+++ b/src/EventLogExpert.EventDbTool/ShowCommand.cs
@@ -52,7 +52,7 @@ public static Command GetCommand()
return showCommand;
}
- private void ShowProviderInfo(string? source, string? filter)
+ internal void ShowProviderInfo(string? source, string? filter)
{
if (!RegexHelper.TryCreate(filter, Logger, out var regex)) { return; }
diff --git a/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/CreateDatabaseCommandTests.cs b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/CreateDatabaseCommandTests.cs
new file mode 100644
index 00000000..217608e1
--- /dev/null
+++ b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/CreateDatabaseCommandTests.cs
@@ -0,0 +1,280 @@
+// // Copyright (c) Microsoft Corporation.
+// // Licensed under the MIT License.
+
+using EventLogExpert.EventDbTool.IntegrationTests.TestUtils;
+using EventLogExpert.EventDbTool.IntegrationTests.TestUtils.Constants;
+using EventLogExpert.Eventing.Logging;
+using EventLogExpert.Eventing.ProviderDatabase;
+using NSubstitute;
+
+namespace EventLogExpert.EventDbTool.IntegrationTests;
+
+public sealed class CreateDatabaseCommandTests : IDisposable
+{
+ private readonly List _tempDirs = [];
+ private readonly List _tempPaths = [];
+
+ [Fact]
+ public void CreateDatabase_WhenExtensionNotDb_LogsErrorAndDoesNotCreateFile()
+ {
+ // Arrange
+ var path = DatabaseTestUtils.CreateTempPath(".txt");
+ _tempPaths.Add(path);
+ var logger = Substitute.For();
+
+ // Act
+ new CreateDatabaseCommand(logger).CreateDatabase(path, source: null, filter: null, skipProvidersInFile: null);
+
+ // Assert
+ Assert.False(File.Exists(path), "No file should be written when the extension is wrong.");
+ logger.Received(1).Error(Arg.Is(h =>
+ h.ToString().Contains("File extension must be .db")));
+ }
+
+ [Fact]
+ public void CreateDatabase_WhenFilterMatchesNoProviders_DoesNotLeaveEmptyDatabaseOnDisk()
+ {
+ // Arrange — source has First+Second, filter excludes both. The command must not leave an
+ // empty .db file behind, because a downstream consumer would read it as "this collection
+ // has zero providers" instead of "this provider set was never collected".
+ var source = CreateTempPath();
+ DatabaseTestUtils.CreateV4Database(source,
+ DatabaseTestUtils.BuildProviderDetails(Constants.FirstProviderName),
+ DatabaseTestUtils.BuildProviderDetails(Constants.SecondProviderName));
+
+ var path = CreateTempPath();
+ var logger = Substitute.For();
+
+ // Act
+ new CreateDatabaseCommand(logger).CreateDatabase(path, source, filter: "ZZZ_NoMatch_ZZZ", skipProvidersInFile: null);
+
+ // Assert
+ Assert.False(File.Exists(path), "No file should be written when zero providers were resolved.");
+ logger.Received(1).Warning(Arg.Is(h =>
+ h.ToString().Contains("No provider details could be resolved") &&
+ h.ToString().Contains("Database was not created")));
+ // No errors should have been logged on this path; an extra error here would be a UX regression.
+ logger.DidNotReceive().Error(Arg.Any());
+ }
+
+ [Fact]
+ public void CreateDatabase_WhenFilterRegexInvalid_LogsErrorAndDoesNotCreateFile()
+ {
+ // Arrange
+ var path = CreateTempPath();
+ var logger = Substitute.For();
+
+ // Act
+ new CreateDatabaseCommand(logger).CreateDatabase(path, source: null, filter: "[unclosed", skipProvidersInFile: null);
+
+ // Assert
+ Assert.False(File.Exists(path), "No file should be written when the filter is invalid.");
+ logger.Received(1).Error(Arg.Is(h =>
+ h.ToString().Contains("Invalid --filter regex")));
+ }
+
+ [Fact]
+ public void CreateDatabase_WhenProviderCountCrossesBatchSize_PersistsEveryProviderWithoutErrors()
+ {
+ // Arrange — exercises the mid-stream FlushHeaderAndBuffer path: at 100 providers the buffer
+ // is flushed, the DbContext is materialized, and subsequent providers are appended in
+ // BatchSize=100 chunks. 101 providers guarantees we cross the boundary AND continue beyond
+ // it, so a regression in the "after first flush" branch would lose providers.
+ const int ProviderCount = 101;
+ var source = CreateTempPath();
+ var providers = Enumerable.Range(0, ProviderCount)
+ .Select(i => DatabaseTestUtils.BuildProviderDetails($"Provider-{i:D4}"))
+ .ToArray();
+ DatabaseTestUtils.CreateV4Database(source, providers);
+
+ var path = CreateTempPath();
+ var logger = Substitute.For();
+
+ // Act
+ new CreateDatabaseCommand(logger).CreateDatabase(path, source, filter: null, skipProvidersInFile: null);
+
+ // Assert
+ Assert.True(File.Exists(path));
+
+ using var verify = new ProviderDbContext(path, true);
+ Assert.Equal(ProviderCount, verify.ProviderDetails.Count());
+ var firstName = verify.ProviderDetails.OrderBy(r => r.ProviderName).First().ProviderName;
+ var lastName = verify.ProviderDetails.OrderBy(r => r.ProviderName).Last().ProviderName;
+ Assert.Equal("Provider-0000", firstName);
+ Assert.Equal($"Provider-{ProviderCount - 1:D4}", lastName);
+ logger.DidNotReceive().Error(Arg.Any());
+ logger.DidNotReceive().Warning(Arg.Any());
+ }
+
+ [Fact]
+ public void CreateDatabase_WhenSkipProvidersInFileExcludesAll_DoesNotLeaveEmptyDatabaseOnDisk()
+ {
+ // Arrange — distinct contract path from the filter case: the skip-source contains every
+ // provider in the source, leaving zero to write. The "no empty .db" guarantee must hold
+ // here too; a regression in the skip-set integration could reintroduce the empty stub.
+ var source = CreateTempPath();
+ DatabaseTestUtils.CreateV4Database(source,
+ DatabaseTestUtils.BuildProviderDetails(Constants.FirstProviderName),
+ DatabaseTestUtils.BuildProviderDetails(Constants.SecondProviderName));
+
+ var skipSource = CreateTempPath();
+ DatabaseTestUtils.CreateV4Database(skipSource,
+ DatabaseTestUtils.BuildProviderDetails(Constants.FirstProviderName),
+ DatabaseTestUtils.BuildProviderDetails(Constants.SecondProviderName));
+
+ var path = CreateTempPath();
+ var logger = Substitute.For();
+
+ // Act
+ new CreateDatabaseCommand(logger).CreateDatabase(path, source, filter: null, skipProvidersInFile: skipSource);
+
+ // Assert
+ Assert.False(File.Exists(path), "No file should be written when the skip-set excludes all providers.");
+ logger.Received(1).Warning(Arg.Is(h =>
+ h.ToString().Contains("No provider details could be resolved") &&
+ h.ToString().Contains("Database was not created")));
+ logger.DidNotReceive().Error(Arg.Any());
+ }
+
+ [Fact]
+ public void CreateDatabase_WhenSkipProvidersInFileResolves_ExcludesThoseProvidersFromOutput()
+ {
+ // Arrange — source has First+Second+Shared. Skip-source has Shared. Output must contain
+ // First+Second only, exercising the skip-set integration with the streaming write path.
+ var source = CreateTempPath();
+ DatabaseTestUtils.CreateV4Database(source,
+ DatabaseTestUtils.BuildProviderDetails(Constants.FirstProviderName),
+ DatabaseTestUtils.BuildProviderDetails(Constants.SecondProviderName),
+ DatabaseTestUtils.BuildProviderDetails(Constants.SharedProviderName));
+
+ var skipSource = CreateTempPath();
+ DatabaseTestUtils.CreateV4Database(skipSource,
+ DatabaseTestUtils.BuildProviderDetails(Constants.SharedProviderName));
+
+ var path = CreateTempPath();
+ var logger = Substitute.For();
+
+ // Act
+ new CreateDatabaseCommand(logger).CreateDatabase(path, source, filter: null, skipProvidersInFile: skipSource);
+
+ // Assert
+ Assert.True(File.Exists(path));
+
+ using var verify = new ProviderDbContext(path, true);
+ var names = verify.ProviderDetails.Select(r => r.ProviderName).OrderBy(n => n).ToList();
+ Assert.Equal(new[] { Constants.FirstProviderName, Constants.SecondProviderName }, names);
+ logger.DidNotReceive().Error(Arg.Any());
+ logger.DidNotReceive().Warning(Arg.Any());
+ }
+
+ [Fact]
+ public void CreateDatabase_WhenSkipProvidersInFileSourceDoesNotExist_LogsErrorAndDoesNotCreateFile()
+ {
+ // Arrange — valid source but invalid skip-source. Validation order matters: a missing skip
+ // file must fail BEFORE we begin writing the output database, so an empty stub is never
+ // left behind.
+ var path = CreateTempPath();
+ var source = CreateTempPath();
+ DatabaseTestUtils.CreateV4Database(source,
+ DatabaseTestUtils.BuildProviderDetails(Constants.FirstProviderName));
+
+ var missingSkipSource = DatabaseTestUtils.CreateTempPath(".db");
+ var logger = Substitute.For();
+
+ // Act
+ new CreateDatabaseCommand(logger).CreateDatabase(path, source, filter: null, skipProvidersInFile: missingSkipSource);
+
+ // Assert
+ Assert.False(File.Exists(path), "No file should be written when the skip-source is missing.");
+ logger.Received(1).Error(Arg.Is(h =>
+ h.ToString().Contains("Source not found") && h.ToString().Contains(missingSkipSource)));
+ }
+
+ [Fact]
+ public void CreateDatabase_WhenSourceDoesNotExist_LogsErrorAndDoesNotCreateFile()
+ {
+ // Arrange
+ var path = CreateTempPath();
+ var missingSource = DatabaseTestUtils.CreateTempPath(".db");
+ var logger = Substitute.For();
+
+ // Act
+ new CreateDatabaseCommand(logger).CreateDatabase(path, source: missingSource, filter: null, skipProvidersInFile: null);
+
+ // Assert
+ Assert.False(File.Exists(path), "No file should be written when the source is missing.");
+ logger.Received(1).Error(Arg.Is(h =>
+ h.ToString().Contains("Source not found") && h.ToString().Contains(missingSource)));
+ }
+
+ [Fact]
+ public void CreateDatabase_WhenSourceProvidersResolved_PersistsAllProvidersAndPreservesOwningPublisher()
+ {
+ // Arrange — one provider has ResolvedFromOwningPublisher set; this round-trips through the
+ // full streaming write path and we verify the persisted DB matches the source contents.
+ var source = CreateTempPath();
+ DatabaseTestUtils.CreateV4Database(source,
+ DatabaseTestUtils.BuildProviderDetails(Constants.FirstProviderName),
+ DatabaseTestUtils.BuildProviderDetails(Constants.SecondProviderName, Constants.OwningPublisherName));
+
+ var path = CreateTempPath();
+ var logger = Substitute.For();
+
+ // Act
+ new CreateDatabaseCommand(logger).CreateDatabase(path, source, filter: null, skipProvidersInFile: null);
+
+ // Assert
+ Assert.True(File.Exists(path), "Output database should be created when providers are resolved.");
+
+ using var verify = new ProviderDbContext(path, true);
+ var rows = verify.ProviderDetails.OrderBy(r => r.ProviderName).ToList();
+ Assert.Equal(2, rows.Count);
+ Assert.Equal(Constants.FirstProviderName, rows[0].ProviderName);
+ Assert.Null(rows[0].ResolvedFromOwningPublisher);
+ Assert.Equal(Constants.SecondProviderName, rows[1].ProviderName);
+ Assert.Equal(Constants.OwningPublisherName, rows[1].ResolvedFromOwningPublisher);
+ // Success path must not surface any errors or warnings; a regression that warned spuriously
+ // (e.g., "no providers resolved" reaching the success branch) would degrade operator trust.
+ logger.DidNotReceive().Error(Arg.Any());
+ logger.DidNotReceive().Warning(Arg.Any());
+ }
+
+ [Fact]
+ public void CreateDatabase_WhenTargetFileAlreadyExists_LogsErrorAndDoesNotOverwrite()
+ {
+ // Arrange — target file already exists with sentinel bytes; the command must not overwrite or truncate.
+ var path = CreateTempPath();
+ var sentinel = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF };
+ File.WriteAllBytes(path, sentinel);
+ var logger = Substitute.For();
+
+ // Act
+ new CreateDatabaseCommand(logger).CreateDatabase(path, source: null, filter: null, skipProvidersInFile: null);
+
+ // Assert
+ Assert.Equal(sentinel, File.ReadAllBytes(path));
+ logger.Received(1).Error(Arg.Is(h =>
+ h.ToString().Contains("file already exists") && h.ToString().Contains(path)));
+ }
+
+ public void Dispose()
+ {
+ foreach (var path in _tempPaths)
+ {
+ DatabaseTestUtils.DeleteDatabaseFile(path);
+ }
+
+ foreach (var dir in _tempDirs)
+ {
+ DatabaseTestUtils.DeleteDirectoryRecursive(dir);
+ }
+ }
+
+ private string CreateTempPath()
+ {
+ var path = DatabaseTestUtils.CreateTempPath();
+ _tempPaths.Add(path);
+
+ return path;
+ }
+}
diff --git a/tests/Unit/EventLogExpert.EventDbTool.Tests/DiffDatabaseCommandTests.cs b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/DiffDatabaseCommandTests.cs
similarity index 95%
rename from tests/Unit/EventLogExpert.EventDbTool.Tests/DiffDatabaseCommandTests.cs
rename to tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/DiffDatabaseCommandTests.cs
index 05c8efab..b1e1a2dd 100644
--- a/tests/Unit/EventLogExpert.EventDbTool.Tests/DiffDatabaseCommandTests.cs
+++ b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/DiffDatabaseCommandTests.cs
@@ -1,13 +1,13 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.
-using EventLogExpert.EventDbTool.Tests.TestUtils;
-using EventLogExpert.EventDbTool.Tests.TestUtils.Constants;
+using EventLogExpert.EventDbTool.IntegrationTests.TestUtils;
+using EventLogExpert.EventDbTool.IntegrationTests.TestUtils.Constants;
using EventLogExpert.Eventing.Logging;
using EventLogExpert.Eventing.ProviderDatabase;
using NSubstitute;
-namespace EventLogExpert.EventDbTool.Tests;
+namespace EventLogExpert.EventDbTool.IntegrationTests;
public sealed class DiffDatabaseCommandTests : IDisposable
{
diff --git a/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/EventLogExpert.EventDbTool.IntegrationTests.csproj b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/EventLogExpert.EventDbTool.IntegrationTests.csproj
new file mode 100644
index 00000000..607e3eda
--- /dev/null
+++ b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/EventLogExpert.EventDbTool.IntegrationTests.csproj
@@ -0,0 +1,23 @@
+
+
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/GlobalUsings.cs b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/GlobalUsings.cs
new file mode 100644
index 00000000..b04ace3e
--- /dev/null
+++ b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/GlobalUsings.cs
@@ -0,0 +1,4 @@
+// // Copyright (c) Microsoft Corporation.
+// // Licensed under the MIT License.
+
+global using Xunit;
diff --git a/tests/Unit/EventLogExpert.EventDbTool.Tests/MergeDatabaseCommandTests.cs b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/MergeDatabaseCommandTests.cs
similarity index 93%
rename from tests/Unit/EventLogExpert.EventDbTool.Tests/MergeDatabaseCommandTests.cs
rename to tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/MergeDatabaseCommandTests.cs
index afafa34a..83aa76f6 100644
--- a/tests/Unit/EventLogExpert.EventDbTool.Tests/MergeDatabaseCommandTests.cs
+++ b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/MergeDatabaseCommandTests.cs
@@ -1,12 +1,12 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.
-using EventLogExpert.EventDbTool.Tests.TestUtils;
-using EventLogExpert.EventDbTool.Tests.TestUtils.Constants;
+using EventLogExpert.EventDbTool.IntegrationTests.TestUtils;
+using EventLogExpert.EventDbTool.IntegrationTests.TestUtils.Constants;
using EventLogExpert.Eventing.Logging;
using NSubstitute;
-namespace EventLogExpert.EventDbTool.Tests;
+namespace EventLogExpert.EventDbTool.IntegrationTests;
public sealed class MergeDatabaseCommandTests : IDisposable
{
diff --git a/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/MtaProviderSourceTests.cs b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/MtaProviderSourceTests.cs
new file mode 100644
index 00000000..9e5f3518
--- /dev/null
+++ b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/MtaProviderSourceTests.cs
@@ -0,0 +1,136 @@
+// // Copyright (c) Microsoft Corporation.
+// // Licensed under the MIT License.
+
+using EventLogExpert.EventDbTool.IntegrationTests.TestUtils;
+using EventLogExpert.Eventing.Logging;
+using NSubstitute;
+
+namespace EventLogExpert.EventDbTool.IntegrationTests;
+
+public sealed class MtaProviderSourceTests : IDisposable
+{
+ private readonly List _tempDirs = [];
+ private readonly List _tempFiles = [];
+
+ [Fact]
+ public void DiscoverProviderNames_WhenEvtxFileMissing_LogsErrorReturnsEmpty()
+ {
+ // Arrange
+ var missing = DatabaseTestUtils.CreateTempPath(".evtx");
+ var logger = Substitute.For();
+
+ // Act
+ var providers = MtaProviderSource.DiscoverProviderNames(missing, logger);
+
+ // Assert
+ Assert.Empty(providers);
+ logger.Received(1).Error(Arg.Is(h =>
+ h.ToString().Contains("Evtx file not found") && h.ToString().Contains(missing)));
+ }
+
+ [Fact]
+ public void DiscoverProviderNames_WhenFilterRegexIsInvalid_LogsErrorReturnsEmpty()
+ {
+ // Arrange
+ var missing = DatabaseTestUtils.CreateTempPath(".evtx");
+ var logger = Substitute.For();
+
+ // Act — invalid pattern is rejected before the evtx is even opened.
+ var providers = MtaProviderSource.DiscoverProviderNames(missing, logger, filter: "[unclosed");
+
+ // Assert
+ Assert.Empty(providers);
+ logger.Received(1).Error(Arg.Is(h =>
+ h.ToString().Contains("Invalid --filter regex")));
+ // No "Evtx file not found" should be logged because we short-circuited on the bad regex.
+ logger.DidNotReceive().Error(Arg.Is(h =>
+ h.ToString().Contains("Evtx file not found")));
+ }
+
+ public void Dispose()
+ {
+ foreach (var dir in _tempDirs)
+ {
+ DatabaseTestUtils.DeleteDirectoryRecursive(dir);
+ }
+
+ foreach (var file in _tempFiles)
+ {
+ DatabaseTestUtils.DeleteDatabaseFile(file);
+ }
+ }
+
+ [Fact]
+ public void FindMtaFiles_WhenLocaleMetaDataContainsMtaFiles_ReturnsAllOrdinalSortedAndLogsCount()
+ {
+ // Arrange
+ var dir = DatabaseTestUtils.CreateTempDirectory();
+ _tempDirs.Add(dir);
+ var evtxPath = Path.Combine(dir, "test.evtx");
+ File.WriteAllBytes(evtxPath, []);
+ var localeDir = Path.Combine(dir, "LocaleMetaData");
+ Directory.CreateDirectory(localeDir);
+
+ // Names chosen so non-ordinal sorts (e.g., culture-aware) would reorder them differently.
+ var b = Path.Combine(localeDir, "B-Provider.MTA");
+ var a = Path.Combine(localeDir, "A-Provider.MTA");
+ var c = Path.Combine(localeDir, "C-Provider.MTA");
+ File.WriteAllBytes(a, [0x00]);
+ File.WriteAllBytes(b, [0x00]);
+ File.WriteAllBytes(c, [0x00]);
+
+ var logger = Substitute.For();
+
+ // Act
+ var files = MtaProviderSource.FindMtaFiles(evtxPath, logger);
+
+ // Assert — ordinal order ensures consistent provider lookup precedence across locales.
+ Assert.Equal(3, files.Count);
+ Assert.Equal(a, files[0]);
+ Assert.Equal(b, files[1]);
+ Assert.Equal(c, files[2]);
+ logger.Received(1).Information(Arg.Is(h =>
+ h.ToString().Contains("3 locale metadata file") && h.ToString().Contains(localeDir)));
+ logger.DidNotReceive().Error(Arg.Any());
+ }
+
+ [Fact]
+ public void FindMtaFiles_WhenLocaleMetaDataDirectoryIsEmpty_LogsErrorReturnsEmpty()
+ {
+ // Arrange
+ var dir = DatabaseTestUtils.CreateTempDirectory();
+ _tempDirs.Add(dir);
+ var evtxPath = Path.Combine(dir, "test.evtx");
+ File.WriteAllBytes(evtxPath, []);
+ var localeDir = Path.Combine(dir, "LocaleMetaData");
+ Directory.CreateDirectory(localeDir);
+ var logger = Substitute.For();
+
+ // Act
+ var files = MtaProviderSource.FindMtaFiles(evtxPath, logger);
+
+ // Assert
+ Assert.Empty(files);
+ logger.Received(1).Error(Arg.Is(h =>
+ h.ToString().Contains("contains no MTA files") && h.ToString().Contains(localeDir)));
+ }
+
+ [Fact]
+ public void FindMtaFiles_WhenLocaleMetaDataDirectoryMissing_LogsErrorReturnsEmpty()
+ {
+ // Arrange — create a temp dir with an evtx file but NO LocaleMetaData subdir.
+ var dir = DatabaseTestUtils.CreateTempDirectory();
+ _tempDirs.Add(dir);
+ var evtxPath = Path.Combine(dir, "test.evtx");
+ File.WriteAllBytes(evtxPath, []);
+ var logger = Substitute.For();
+
+ // Act
+ var files = MtaProviderSource.FindMtaFiles(evtxPath, logger);
+
+ // Assert
+ Assert.Empty(files);
+ logger.Received(1).Error(Arg.Is(h =>
+ h.ToString().Contains("No LocaleMetaData folder") && h.ToString().Contains(evtxPath)));
+ }
+}
diff --git a/tests/Unit/EventLogExpert.EventDbTool.Tests/ProviderSourceTests.cs b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/ProviderSourceTests.cs
similarity index 96%
rename from tests/Unit/EventLogExpert.EventDbTool.Tests/ProviderSourceTests.cs
rename to tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/ProviderSourceTests.cs
index 2176830d..8a8d42c0 100644
--- a/tests/Unit/EventLogExpert.EventDbTool.Tests/ProviderSourceTests.cs
+++ b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/ProviderSourceTests.cs
@@ -1,12 +1,12 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.
-using EventLogExpert.EventDbTool.Tests.TestUtils;
-using EventLogExpert.EventDbTool.Tests.TestUtils.Constants;
+using EventLogExpert.EventDbTool.IntegrationTests.TestUtils;
+using EventLogExpert.EventDbTool.IntegrationTests.TestUtils.Constants;
using EventLogExpert.Eventing.Logging;
using NSubstitute;
-namespace EventLogExpert.EventDbTool.Tests;
+namespace EventLogExpert.EventDbTool.IntegrationTests;
public sealed class ProviderSourceTests : IDisposable
{
diff --git a/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/ShowCommandTests.cs b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/ShowCommandTests.cs
new file mode 100644
index 00000000..a051ffc7
--- /dev/null
+++ b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/ShowCommandTests.cs
@@ -0,0 +1,117 @@
+// // Copyright (c) Microsoft Corporation.
+// // Licensed under the MIT License.
+
+using EventLogExpert.EventDbTool.IntegrationTests.TestUtils;
+using EventLogExpert.EventDbTool.IntegrationTests.TestUtils.Constants;
+using EventLogExpert.Eventing.Logging;
+using NSubstitute;
+
+namespace EventLogExpert.EventDbTool.IntegrationTests;
+
+public sealed class ShowCommandTests : IDisposable
+{
+ private readonly List _tempPaths = [];
+
+ public void Dispose()
+ {
+ foreach (var path in _tempPaths)
+ {
+ DatabaseTestUtils.DeleteDatabaseFile(path);
+ }
+ }
+
+ [Fact]
+ public void ShowProviderInfo_WhenFilterRegexInvalid_LogsErrorAndReturns()
+ {
+ // Arrange — pass a source so we never hit the local-machine path (non-deterministic).
+ var source = CreateTempPath();
+ DatabaseTestUtils.CreateV4Database(source,
+ DatabaseTestUtils.BuildProviderDetails(Constants.FirstProviderName));
+
+ var logger = Substitute.For();
+
+ // Act
+ new ShowCommand(logger).ShowProviderInfo(source, filter: "[unclosed");
+
+ // Assert
+ logger.Received(1).Error(Arg.Is(h =>
+ h.ToString().Contains("Invalid --filter regex")));
+ logger.DidNotReceive().Information(Arg.Any());
+ logger.DidNotReceive().Warning(Arg.Any());
+ }
+
+ [Fact]
+ public void ShowProviderInfo_WhenSourceDoesNotExist_LogsErrorAndDoesNotReadAnything()
+ {
+ // Arrange
+ var missing = DatabaseTestUtils.CreateTempPath(".db");
+ var logger = Substitute.For();
+
+ // Act
+ new ShowCommand(logger).ShowProviderInfo(missing, filter: null);
+
+ // Assert
+ logger.Received(1).Error(Arg.Is(h =>
+ h.ToString().Contains("Source not found") && h.ToString().Contains(missing)));
+ logger.DidNotReceive().Information(Arg.Any());
+ }
+
+ [Fact]
+ public void ShowProviderInfo_WhenSourceFilterMatchesNoProviders_LogsNoProvidersWarningOnly()
+ {
+ // Arrange — source has providers but the filter matches none. The "no providers found"
+ // message is the contract for "your filter is too narrow" UX, distinct from "source missing".
+ var source = CreateTempPath();
+ DatabaseTestUtils.CreateV4Database(source,
+ DatabaseTestUtils.BuildProviderDetails(Constants.FirstProviderName),
+ DatabaseTestUtils.BuildProviderDetails(Constants.SecondProviderName));
+
+ var logger = Substitute.For();
+
+ // Act
+ new ShowCommand(logger).ShowProviderInfo(source, filter: "ZZZ_NoMatch_ZZZ");
+
+ // Assert
+ logger.Received(1).Warning(Arg.Is(h =>
+ h.ToString().Contains("No providers found")));
+ logger.DidNotReceive().Error(Arg.Any());
+ // No detail header should be emitted when there is nothing to list.
+ logger.DidNotReceive().Information(Arg.Is(h =>
+ h.ToString().Contains("Provider Name")));
+ }
+
+ [Fact]
+ public void ShowProviderInfo_WhenSourceHasProviders_LogsHeaderAndOneRowPerProvider()
+ {
+ // Arrange — two providers. We assert that the header appears exactly once and a row for
+ // each provider name appears, locking in the streaming "header + per-provider row" output
+ // contract that downstream tooling and humans both depend on.
+ var source = CreateTempPath();
+ DatabaseTestUtils.CreateV4Database(source,
+ DatabaseTestUtils.BuildProviderDetails(Constants.FirstProviderName),
+ DatabaseTestUtils.BuildProviderDetails(Constants.SecondProviderName));
+
+ var logger = Substitute.For();
+
+ // Act
+ new ShowCommand(logger).ShowProviderInfo(source, filter: null);
+
+ // Assert
+ logger.Received(1).Information(Arg.Is(h =>
+ h.ToString().Contains("Provider Name") && h.ToString().Contains("Events")));
+ logger.Received(1).Information(Arg.Is(h =>
+ h.ToString().Contains(Constants.FirstProviderName)));
+ logger.Received(1).Information(Arg.Is(h =>
+ h.ToString().Contains(Constants.SecondProviderName)));
+ logger.DidNotReceive().Error(Arg.Any());
+ logger.DidNotReceive().Warning(Arg.Any());
+ }
+
+ private string CreateTempPath()
+ {
+ var path = DatabaseTestUtils.CreateTempPath();
+ _tempPaths.Add(path);
+
+ return path;
+ }
+}
diff --git a/tests/Unit/EventLogExpert.EventDbTool.Tests/TestUtils/Constants/Constants.Database.cs b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/TestUtils/Constants/Constants.Database.cs
similarity index 83%
rename from tests/Unit/EventLogExpert.EventDbTool.Tests/TestUtils/Constants/Constants.Database.cs
rename to tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/TestUtils/Constants/Constants.Database.cs
index cdec3c49..50d18e07 100644
--- a/tests/Unit/EventLogExpert.EventDbTool.Tests/TestUtils/Constants/Constants.Database.cs
+++ b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/TestUtils/Constants/Constants.Database.cs
@@ -1,7 +1,7 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.
-namespace EventLogExpert.EventDbTool.Tests.TestUtils.Constants;
+namespace EventLogExpert.EventDbTool.IntegrationTests.TestUtils.Constants;
public sealed partial class Constants
{
diff --git a/tests/Unit/EventLogExpert.EventDbTool.Tests/TestUtils/DatabaseTestUtils.cs b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/TestUtils/DatabaseTestUtils.cs
similarity index 66%
rename from tests/Unit/EventLogExpert.EventDbTool.Tests/TestUtils/DatabaseTestUtils.cs
rename to tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/TestUtils/DatabaseTestUtils.cs
index fa8dbed6..d9949221 100644
--- a/tests/Unit/EventLogExpert.EventDbTool.Tests/TestUtils/DatabaseTestUtils.cs
+++ b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/TestUtils/DatabaseTestUtils.cs
@@ -3,9 +3,10 @@
using EventLogExpert.Eventing.ProviderDatabase;
using EventLogExpert.Eventing.Providers;
+using EventLogExpert.Eventing.TestUtils;
using Microsoft.Data.Sqlite;
-namespace EventLogExpert.EventDbTool.Tests.TestUtils;
+namespace EventLogExpert.EventDbTool.IntegrationTests.TestUtils;
internal static class DatabaseTestUtils
{
@@ -77,63 +78,7 @@ public static void CreateV4Database(string dbPath, params ProviderDetails[] prov
context.SaveChanges();
}
- public static void DeleteDatabaseFile(string path)
- {
- try
- {
- if (!File.Exists(path)) { return; }
-
- SqliteConnection.ClearAllPools();
- GC.Collect();
- GC.WaitForPendingFinalizers();
- GC.Collect();
-
- for (var i = 0; i < 10; i++)
- {
- try
- {
- File.Delete(path);
- break;
- }
- catch (IOException)
- {
- Thread.Sleep(200);
- }
- }
- }
- catch
- {
- // Best effort cleanup.
- }
- }
+ public static void DeleteDatabaseFile(string path) => SqliteTestDb.Delete(path);
- public static void DeleteDirectoryRecursive(string path)
- {
- try
- {
- if (!Directory.Exists(path)) { return; }
-
- SqliteConnection.ClearAllPools();
- GC.Collect();
- GC.WaitForPendingFinalizers();
- GC.Collect();
-
- for (var i = 0; i < 10; i++)
- {
- try
- {
- Directory.Delete(path, recursive: true);
- break;
- }
- catch (IOException)
- {
- Thread.Sleep(200);
- }
- }
- }
- catch
- {
- // Best effort cleanup.
- }
- }
+ public static void DeleteDirectoryRecursive(string path) => SqliteTestDb.DeleteDirectory(path);
}
diff --git a/tests/Unit/EventLogExpert.EventDbTool.Tests/UpgradeDatabaseCommandTests.cs b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/UpgradeDatabaseCommandTests.cs
similarity index 97%
rename from tests/Unit/EventLogExpert.EventDbTool.Tests/UpgradeDatabaseCommandTests.cs
rename to tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/UpgradeDatabaseCommandTests.cs
index ee375cf9..c738ddbc 100644
--- a/tests/Unit/EventLogExpert.EventDbTool.Tests/UpgradeDatabaseCommandTests.cs
+++ b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/UpgradeDatabaseCommandTests.cs
@@ -1,12 +1,12 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.
-using EventLogExpert.EventDbTool.Tests.TestUtils;
+using EventLogExpert.EventDbTool.IntegrationTests.TestUtils;
using EventLogExpert.Eventing.Logging;
using EventLogExpert.Eventing.ProviderDatabase;
using NSubstitute;
-namespace EventLogExpert.EventDbTool.Tests;
+namespace EventLogExpert.EventDbTool.IntegrationTests;
public sealed class UpgradeDatabaseCommandTests : IDisposable
{
diff --git a/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/xunit.runner.json b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/xunit.runner.json
new file mode 100644
index 00000000..77778efd
--- /dev/null
+++ b/tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/xunit.runner.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://xunit.net/schema/v3/xunit.runner.schema.json",
+ "parallelizeAssembly": false,
+ "parallelizeTestCollections": false
+}
diff --git a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/EventLogExpert.Eventing.IntegrationTests.csproj b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/EventLogExpert.Eventing.IntegrationTests.csproj
index c9c039b8..12ca6fc4 100644
--- a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/EventLogExpert.Eventing.IntegrationTests.csproj
+++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/EventLogExpert.Eventing.IntegrationTests.csproj
@@ -8,6 +8,16 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Interop/NativeErrorResolverTests.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Interop/NativeErrorResolverTests.cs
similarity index 97%
rename from tests/Unit/EventLogExpert.Eventing.Tests/Interop/NativeErrorResolverTests.cs
rename to tests/Integration/EventLogExpert.Eventing.IntegrationTests/Interop/NativeErrorResolverTests.cs
index c50f9fcf..d681618c 100644
--- a/tests/Unit/EventLogExpert.Eventing.Tests/Interop/NativeErrorResolverTests.cs
+++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Interop/NativeErrorResolverTests.cs
@@ -3,7 +3,7 @@
using EventLogExpert.Eventing.Interop;
-namespace EventLogExpert.Eventing.Tests.Interop;
+namespace EventLogExpert.Eventing.IntegrationTests.Interop;
public sealed class NativeErrorResolverTests
{
diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderDbContextTests.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/ProviderDatabase/ProviderDbContextTests.cs
similarity index 98%
rename from tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderDbContextTests.cs
rename to tests/Integration/EventLogExpert.Eventing.IntegrationTests/ProviderDatabase/ProviderDbContextTests.cs
index 7e98d6db..22b73905 100644
--- a/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderDbContextTests.cs
+++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/ProviderDatabase/ProviderDbContextTests.cs
@@ -4,13 +4,13 @@
using EventLogExpert.Eventing.Logging;
using EventLogExpert.Eventing.ProviderDatabase;
using EventLogExpert.Eventing.Providers;
-using EventLogExpert.Eventing.Tests.TestUtils;
+using EventLogExpert.Eventing.TestUtils;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using NSubstitute;
using System.Text;
-namespace EventLogExpert.Eventing.Tests.ProviderDatabase;
+namespace EventLogExpert.Eventing.IntegrationTests.ProviderDatabase;
public sealed class ProviderDbContextTests : IDisposable
{
@@ -415,60 +415,60 @@ public void PerformUpgradeIfNeeded_WithV1Schema_ThrowsAndPreservesLegacyTable()
}
[Fact]
- public void PerformUpgradeIfNeeded_WithV2SchemaAndNullParameters_Throws()
+ public void PerformUpgradeIfNeeded_WithV2Schema_Throws()
{
- // Arrange — V2 row with NULL Parameters column; previously this would have silently
- // round-tripped to an empty list. Now it must surface the same hard-fail as any other V2 row.
+ // Arrange — V2 row stores Parameters as JSON TEXT.
var dbPath = CreateTempDatabasePath();
SeedLegacySchema(dbPath, includeParameters: true, parametersType: "TEXT", messagesType: "TEXT");
InsertLegacyRow(
dbPath,
- providerName: "V2NullParams",
+ providerName: "V2Provider",
messagesJson: "[]",
- parametersJson: null,
+ parametersJson: "[{\"ShortId\":2,\"LogLink\":null,\"RawId\":2,\"Tag\":null,\"Template\":null,\"Text\":\"param-text\"}]",
eventsJson: "[]",
keywordsJson: "{}",
opcodesJson: "{}",
tasksJson: "{}");
// Act + Assert
- using var context = new ProviderDbContext(dbPath, false);
- var thrown = Assert.Throws(() => context.PerformUpgradeIfNeeded());
+ DatabaseUpgradeException? thrown;
+ using (var context = new ProviderDbContext(dbPath, false))
+ {
+ thrown = Assert.Throws(() => context.PerformUpgradeIfNeeded());
+ }
+
Assert.Contains("v2", thrown.Reason);
+ Assert.Contains("no longer supported", thrown.Reason);
+ // Original V2 row preserved; detection still reports v2.
+ using var verify = new ProviderDbContext(dbPath, true);
+ var stateAfter = verify.IsUpgradeNeeded();
+ Assert.Equal(2, stateAfter.CurrentVersion);
AssertProviderDetailsRowCount(dbPath, expectedRows: 1);
}
[Fact]
- public void PerformUpgradeIfNeeded_WithV2Schema_Throws()
+ public void PerformUpgradeIfNeeded_WithV2SchemaAndNullParameters_Throws()
{
- // Arrange — V2 row stores Parameters as JSON TEXT.
+ // Arrange — V2 row with NULL Parameters column; previously this would have silently
+ // round-tripped to an empty list. Now it must surface the same hard-fail as any other V2 row.
var dbPath = CreateTempDatabasePath();
SeedLegacySchema(dbPath, includeParameters: true, parametersType: "TEXT", messagesType: "TEXT");
InsertLegacyRow(
dbPath,
- providerName: "V2Provider",
+ providerName: "V2NullParams",
messagesJson: "[]",
- parametersJson: "[{\"ShortId\":2,\"LogLink\":null,\"RawId\":2,\"Tag\":null,\"Template\":null,\"Text\":\"param-text\"}]",
+ parametersJson: null,
eventsJson: "[]",
keywordsJson: "{}",
opcodesJson: "{}",
tasksJson: "{}");
// Act + Assert
- DatabaseUpgradeException? thrown;
- using (var context = new ProviderDbContext(dbPath, false))
- {
- thrown = Assert.Throws(() => context.PerformUpgradeIfNeeded());
- }
-
+ using var context = new ProviderDbContext(dbPath, false);
+ var thrown = Assert.Throws(() => context.PerformUpgradeIfNeeded());
Assert.Contains("v2", thrown.Reason);
- Assert.Contains("no longer supported", thrown.Reason);
- // Original V2 row preserved; detection still reports v2.
- using var verify = new ProviderDbContext(dbPath, true);
- var stateAfter = verify.IsUpgradeNeeded();
- Assert.Equal(2, stateAfter.CurrentVersion);
AssertProviderDetailsRowCount(dbPath, expectedRows: 1);
}
@@ -1040,35 +1040,7 @@ private static void AssertProviderDetailsRowCount(string dbPath, int expectedRow
Assert.Equal(expectedRows, count);
}
- private static void DeleteDatabaseFile(string path)
- {
- try
- {
- if (!File.Exists(path)) { return; }
-
- SqliteConnection.ClearAllPools();
- GC.Collect();
- GC.WaitForPendingFinalizers();
- GC.Collect();
-
- for (int i = 0; i < 10; i++)
- {
- try
- {
- File.Delete(path);
- break;
- }
- catch (IOException)
- {
- Thread.Sleep(200);
- }
- }
- }
- catch
- {
- // Cleanup is best effort
- }
- }
+ private static void DeleteDatabaseFile(string path) => SqliteTestDb.Delete(path);
private static void InsertLegacyRow(
string dbPath,
diff --git a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/EventMessageProviderIntegrationTests.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/EventMessageProviderIntegrationTests.cs
index 83ca3034..c8a70a3b 100644
--- a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/EventMessageProviderIntegrationTests.cs
+++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/EventMessageProviderIntegrationTests.cs
@@ -1,11 +1,11 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.
-using EventLogExpert.Eventing.IntegrationTests.TestUtils.Constants;
using EventLogExpert.Eventing.Interop;
using EventLogExpert.Eventing.Logging;
using EventLogExpert.Eventing.Providers;
using EventLogExpert.Eventing.Readers;
+using EventLogExpert.Eventing.TestUtils.Constants;
using NSubstitute;
using System.Globalization;
using System.Runtime.InteropServices;
@@ -68,22 +68,6 @@ public void LoadProviderDetails_ShouldLogProviderLoadingAttempt()
mockLogger.Received().Debug(Arg.Any());
}
- [Fact]
- public void LoadProviderDetails_WhenCalledMultipleTimes_ShouldReturnConsistentResults()
- {
- // Arrange
- EventMessageProvider provider = new(Constants.TestProviderName);
-
- // Act
- var details1 = provider.LoadProviderDetails();
- var details2 = provider.LoadProviderDetails();
-
- // Assert
- Assert.NotNull(details1);
- Assert.NotNull(details2);
- Assert.Equal(details1.ProviderName, details2.ProviderName);
- }
-
[Fact]
public void LoadProviderDetails_WhenCalled_ShouldHaveNonNullCollections()
{
@@ -117,6 +101,22 @@ public void LoadProviderDetails_WhenCalled_ShouldReturnProviderDetails()
Assert.Equal(Constants.TestProviderName, details.ProviderName);
}
+ [Fact]
+ public void LoadProviderDetails_WhenCalledMultipleTimes_ShouldReturnConsistentResults()
+ {
+ // Arrange
+ EventMessageProvider provider = new(Constants.TestProviderName);
+
+ // Act
+ var details1 = provider.LoadProviderDetails();
+ var details2 = provider.LoadProviderDetails();
+
+ // Assert
+ Assert.NotNull(details1);
+ Assert.NotNull(details2);
+ Assert.Equal(details1.ProviderName, details2.ProviderName);
+ }
+
[Fact]
public void LoadProviderDetails_WhenChannelOwningPublisherUnknown_ShouldReturnEmptyDetailsWithoutFallback()
{
@@ -139,9 +139,10 @@ public void LoadProviderDetails_WhenLegacyLoadReturnsNoMessages_ShouldFallBackTo
Assert.SkipUnless(
TryFindProviderWithEmptyLegacyAndWorkingModern(out var providerName),
"Test requires a provider whose legacy registry entries load zero messages and whose modern publisher metadata exposes a loadable MessageFilePath. Common on dev machines but not guaranteed.");
+ Assert.NotNull(providerName);
var mockLogger = Substitute.For();
- EventMessageProvider provider = new(providerName!, logger: mockLogger);
+ EventMessageProvider provider = new(providerName, logger: mockLogger);
var details = provider.LoadProviderDetails();
@@ -175,8 +176,9 @@ public void LoadProviderDetails_WhenProviderNameIsActuallyAChannelPath_ShouldFal
Assert.SkipUnless(
TryFindChannelWithDistinctOwningPublisher(out var channelName, out var owningPublisher),
"Test requires a registered channel whose owning publisher differs from the channel path itself.");
+ Assert.NotNull(channelName);
- EventMessageProvider provider = new(channelName!);
+ EventMessageProvider provider = new(channelName);
var details = provider.LoadProviderDetails();
diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/EventMessageProviderTests.cs
similarity index 55%
rename from tests/Unit/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs
rename to tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/EventMessageProviderTests.cs
index 1637b07e..be55c1d5 100644
--- a/tests/Unit/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs
+++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/EventMessageProviderTests.cs
@@ -3,25 +3,13 @@
using EventLogExpert.Eventing.Logging;
using EventLogExpert.Eventing.Providers;
-using EventLogExpert.Eventing.Tests.TestUtils.Constants;
+using EventLogExpert.Eventing.TestUtils.Constants;
using NSubstitute;
-namespace EventLogExpert.Eventing.Tests.Providers;
+namespace EventLogExpert.Eventing.IntegrationTests.Providers;
public sealed class EventMessageProviderTests
{
- [Theory]
- [InlineData(Constants.TestProviderName)]
- [InlineData(Constants.TestProviderLongName)]
- public void Constructor_WhenDifferentProviderNames_ShouldCreateInstances(string providerName)
- {
- // Arrange & Act
- EventMessageProvider provider = new(providerName);
-
- // Assert
- Assert.NotNull(provider);
- }
-
[Fact]
public void LoadMessagesFromFiles_WhenDuplicateFiles_ShouldProcessAll()
{
@@ -68,19 +56,6 @@ public void LoadMessagesFromFiles_WhenFileListIsNull_ShouldThrowArgumentNullExce
EventMessageProvider.LoadMessagesFromFiles(null!, Constants.TestProviderName, mockLogger));
}
- [Fact]
- public void LoadMessagesFromFiles_WhenFilePathHasMultipleBackslashes_ShouldExtractFileName()
- {
- // Arrange
- var filesWithPath = new[] { Constants.NonExistentDllFullPath };
-
- // Act
- var messages = EventMessageProvider.LoadMessagesFromFiles(filesWithPath, Constants.TestProviderName);
-
- // Assert
- Assert.NotNull(messages);
- }
-
[Fact]
public void LoadMessagesFromFiles_WhenInvalidFile_ShouldLogWarning()
{
@@ -117,49 +92,4 @@ public void LoadMessagesFromFiles_WhenInvalidFile_ShouldReturnEmptyList()
Assert.NotNull(messages);
Assert.Empty(messages);
}
-
- [Fact]
- public void LoadMessagesFromFiles_WhenMultipleInvalidFiles_ShouldLogMultipleWarnings()
- {
- // Arrange
- var invalidFiles = new[] { Constants.NonExistentDll, Constants.NonExistentDll };
- var mockLogger = Substitute.For();
-
- // Act
- EventMessageProvider.LoadMessagesFromFiles(invalidFiles, Constants.TestProviderName, mockLogger);
-
- // Assert: each input that fails the primary MUI-aware load produces a debug log that
- // begins with "LoadLibraryEx failed for {file}". Asserting per-input presence (with the
- // filename in the message) is robust to future changes in the number of fallback attempts
- // or extra diagnostic lines per input — only the primary-attempt failure log is
- // contractually guaranteed to fire once per input here.
- mockLogger.Received(invalidFiles.Length)
- .Debug(Arg.Is(h =>
- h.ToString().Contains("LoadLibraryEx failed") &&
- h.ToString().Contains(Constants.NonExistentDll)));
- }
-
- [Fact]
- public void LoadMessagesFromFiles_WhenMultipleInvalidFiles_ShouldReturnEmptyList()
- {
- // Arrange
- var invalidFiles = new[] { Constants.NonExistentDll, Constants.NonExistentDll, Constants.NonExistentDll };
-
- // Act
- var messages = EventMessageProvider.LoadMessagesFromFiles(invalidFiles, Constants.TestProviderName);
-
- // Assert
- Assert.NotNull(messages);
- Assert.Empty(messages);
- }
-
- [Fact]
- public void LoadMessagesFromFiles_WhenProviderNameProvided_ShouldIncludeInMessages()
- {
- // Arrange & Act
- var messages = EventMessageProvider.LoadMessagesFromFiles([], Constants.TestProviderName);
-
- // Assert
- Assert.NotNull(messages);
- }
}
diff --git a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/ProviderMetadataTests.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/ProviderMetadataTests.cs
index e2ce5765..476daed1 100644
--- a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/ProviderMetadataTests.cs
+++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/ProviderMetadataTests.cs
@@ -1,9 +1,9 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.
-using EventLogExpert.Eventing.IntegrationTests.TestUtils.Constants;
using EventLogExpert.Eventing.Logging;
using EventLogExpert.Eventing.Providers;
+using EventLogExpert.Eventing.TestUtils.Constants;
using NSubstitute;
using System.Collections.ObjectModel;
@@ -29,8 +29,11 @@ public async Task Channels_WhenAccessedConcurrently_ShouldReturnValidData()
// Assert
var results = tasks.Select(t => t.Result).ToList();
- Assert.All(results, Assert.NotNull);
- Assert.All(results, r => Assert.NotEmpty(r!));
+ Assert.All(results, r =>
+ {
+ Assert.NotNull(r);
+ Assert.NotEmpty(r);
+ });
}
[Fact]
@@ -434,8 +437,9 @@ public void MessageFilePath_WhenManifestUsesEnvironmentVariables_ShouldReturnExp
var metadata = ProviderMetadata.Create(Constants.SecurityAuditingLogName);
Assert.SkipUnless(metadata is not null, "Test requires Microsoft-Windows-Security-Auditing provider on the host.");
+ Assert.NotNull(metadata);
- var messageFilePath = metadata!.MessageFilePath;
+ var messageFilePath = metadata.MessageFilePath;
Assert.NotNull(messageFilePath);
Assert.DoesNotContain("%", messageFilePath);
@@ -501,8 +505,11 @@ public async Task Opcodes_WhenAccessedConcurrently_ShouldReturnValidData()
// Assert
var results = tasks.Select(t => t.Result).ToList();
- Assert.All(results, Assert.NotNull);
- Assert.All(results, r => Assert.NotEmpty(r!));
+ Assert.All(results, r =>
+ {
+ Assert.NotNull(r);
+ Assert.NotEmpty(r);
+ });
}
[Fact]
@@ -608,8 +615,9 @@ public void ParameterFilePath_WhenManifestUsesEnvironmentVariables_ShouldReturnE
var metadata = ProviderMetadata.Create(Constants.SecurityAuditingLogName);
Assert.SkipUnless(metadata is not null, "Test requires Microsoft-Windows-Security-Auditing provider on the host.");
+ Assert.NotNull(metadata);
- var parameterFilePath = metadata!.ParameterFilePath;
+ var parameterFilePath = metadata.ParameterFilePath;
Assert.NotNull(parameterFilePath);
Assert.DoesNotContain("%", parameterFilePath);
@@ -647,8 +655,11 @@ public async Task Tasks_WhenAccessedConcurrently_ShouldReturnValidData()
// Assert
var results = tasks.Select(t => t.Result).ToList();
- Assert.All(results, Assert.NotNull);
- Assert.All(results, r => Assert.NotEmpty(r!));
+ Assert.All(results, r =>
+ {
+ Assert.NotNull(r);
+ Assert.NotEmpty(r);
+ });
}
[Fact]
diff --git a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs
index 1d799be8..30aad112 100644
--- a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs
+++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs
@@ -2,9 +2,9 @@
// // Licensed under the MIT License.
using EventLogExpert.Eventing.Common.Channels;
-using EventLogExpert.Eventing.IntegrationTests.TestUtils.Constants;
using EventLogExpert.Eventing.Logging;
using EventLogExpert.Eventing.Providers;
+using EventLogExpert.Eventing.TestUtils.Constants;
using Microsoft.Win32;
using NSubstitute;
@@ -43,6 +43,26 @@ public void Constructor_WhenCreatedMultipleTimes_ShouldCreateDifferentInstances(
Assert.NotSame(provider1, provider2);
}
+ [Fact]
+ public void GetMessageFilesForLegacyProvider_WhenCalled_ShouldNotIncludeSysFiles()
+ {
+ // Arrange
+ var provider = new RegistryProvider();
+
+ // Act
+ var result = FindAnyLegacyProviderFiles(provider);
+
+ // Assert
+ Assert.NotEmpty(result);
+
+ Assert.All(result,
+ path =>
+ {
+ var extension = Path.GetExtension(path).ToLower();
+ Assert.NotEqual(".sys", extension);
+ });
+ }
+
[Fact]
public void GetMessageFilesForLegacyProvider_WhenCalledWithLogger_ShouldLogTrace()
{
@@ -74,26 +94,6 @@ public void GetMessageFilesForLegacyProvider_WhenCalledWithoutLogger_ShouldNotTh
Assert.NotNull(result);
}
- [Fact]
- public void GetMessageFilesForLegacyProvider_WhenCalled_ShouldNotIncludeSysFiles()
- {
- // Arrange
- var provider = new RegistryProvider();
-
- // Act
- var result = FindAnyLegacyProviderFiles(provider);
-
- // Assert
- Assert.NotEmpty(result);
-
- Assert.All(result,
- path =>
- {
- var extension = Path.GetExtension(path).ToLower();
- Assert.NotEqual(".sys", extension);
- });
- }
-
[Theory]
[InlineData(Constants.ApplicationLogName)]
[InlineData(Constants.SystemLogName)]
diff --git a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogInformationTests.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogInformationTests.cs
index ac3a777b..e6d51b81 100644
--- a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogInformationTests.cs
+++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogInformationTests.cs
@@ -2,8 +2,8 @@
// // Licensed under the MIT License.
using EventLogExpert.Eventing.Common.Channels;
-using EventLogExpert.Eventing.IntegrationTests.TestUtils.Constants;
using EventLogExpert.Eventing.Readers;
+using EventLogExpert.Eventing.TestUtils.Constants;
namespace EventLogExpert.Eventing.IntegrationTests.Readers;
diff --git a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogReaderTests.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogReaderTests.cs
index 41b35138..c6509292 100644
--- a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogReaderTests.cs
+++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogReaderTests.cs
@@ -2,77 +2,27 @@
// // Licensed under the MIT License.
using EventLogExpert.Eventing.Common.Channels;
-using EventLogExpert.Eventing.IntegrationTests.TestUtils.Constants;
using EventLogExpert.Eventing.Readers;
+using EventLogExpert.Eventing.TestUtils.Constants;
namespace EventLogExpert.Eventing.IntegrationTests.Readers;
public sealed class EventLogReaderTests
{
- [Fact]
- public void Constructor_WhenApplicationLog_ShouldNotThrow()
- {
- // Arrange & Act
- using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel);
-
- // Assert
- Assert.NotNull(reader);
- }
-
- [Fact]
- public void Constructor_WhenEmptyLogName_ShouldFailToReadEvents()
- {
- // Arrange & Act
- using var reader = new EventLogReader(string.Empty, LogPathType.Channel);
-
- // Assert - TryGetEvents must fail
- bool success = reader.TryGetEvents(out var events);
-
- Assert.False(success, "TryGetEvents should return false for empty log name");
- Assert.NotNull(events);
- Assert.Empty(events);
- }
-
- [Fact]
- public void Constructor_WhenInvalidLog_ShouldFailToReadEvents()
- {
- // Arrange
- var invalidLogName = "NonExistentLog_" + Guid.NewGuid();
-
- // Act
- using var reader = new EventLogReader(invalidLogName, LogPathType.Channel);
-
- // Assert - TryGetEvents must fail
- bool success = reader.TryGetEvents(out var events);
-
- Assert.False(success, "TryGetEvents should return false for invalid log name");
- Assert.NotNull(events);
- Assert.Empty(events);
- }
-
- [Fact]
- public void Constructor_WhenMultipleInstances_ShouldCreateIndependentReaders()
- {
- // Arrange & Act
- using var reader1 = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel);
- using var reader2 = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel);
-
- // Assert
- Assert.NotNull(reader1);
- Assert.NotNull(reader2);
- Assert.NotSame(reader1, reader2);
- }
-
- [Fact]
- public void Constructor_WhenNullLogName_ShouldFailToReadEvents()
+ [Theory]
+ [InlineData("", "empty log name")]
+ [InlineData(null, "null log name")]
+ [InlineData("Invalid<>Log|Name", "log name with special characters")]
+ [InlineData("NonExistentLog_TestSentinel_8e9c2b4a", "non-existent log name")]
+ public void Constructor_WhenInvalidLogName_ShouldFailToReadEvents(string? logName, string scenario)
{
// Arrange & Act
- using var reader = new EventLogReader(null!, LogPathType.Channel);
+ using var reader = new EventLogReader(logName!, LogPathType.Channel);
- // Assert - TryGetEvents must fail
+ // Assert - TryGetEvents must fail without throwing
bool success = reader.TryGetEvents(out var events);
- Assert.False(success, "TryGetEvents should return false for null log name");
+ Assert.False(success, $"TryGetEvents should return false for {scenario}");
Assert.NotNull(events);
Assert.Empty(events);
}
@@ -91,43 +41,18 @@ public void Constructor_WhenPathTypeLogName_ShouldQueryByLogName()
Assert.All(events, evt => Assert.Equal(LogPathType.Channel, evt.LogPathType));
}
- [Fact]
- public void Constructor_WhenRenderXmlFalse_ShouldNotThrow()
- {
- // Arrange & Act
- using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel, renderXml: false);
-
- // Assert
- Assert.NotNull(reader);
- }
-
- [Fact]
- public void Constructor_WhenRenderXmlTrue_ShouldNotThrow()
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void Constructor_WhenRenderXml_ShouldNotThrow(bool renderXml)
{
// Arrange & Act
- using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel, renderXml: true);
+ using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel, renderXml: renderXml);
// Assert
Assert.NotNull(reader);
}
- [Fact]
- public void Constructor_WhenSpecialCharactersInLogName_ShouldFailToReadEvents()
- {
- // Arrange
- var invalidLogName = "Invalid<>Log|Name";
-
- // Act
- using var reader = new EventLogReader(invalidLogName, LogPathType.Channel);
-
- // Assert - TryGetEvents must fail
- bool success = reader.TryGetEvents(out var events);
-
- Assert.False(success, "TryGetEvents should return false for log name with special characters");
- Assert.NotNull(events);
- Assert.Empty(events);
- }
-
[Fact]
public void Dispose_AfterDispose_TryGetEventsShouldThrow()
{
@@ -153,16 +78,6 @@ public void Dispose_WhenCalledMultipleTimes_ShouldNotThrow()
reader.Dispose();
}
- [Fact]
- public void Dispose_WhenCalled_ShouldNotThrow()
- {
- // Arrange
- var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel);
-
- // Act & Assert
- reader.Dispose();
- }
-
[Fact]
public void EndOfResults_AfterExhaustion_ShouldKeepBookmarkAndNotSetLastErrorCode()
{
@@ -329,34 +244,34 @@ public void TryGetEvents_WhenApplicationLog_ShouldReturnValidEventRecords()
}
[Fact]
- public void TryGetEvents_WhenBatchSize10_ShouldReturnUpTo10Events()
+ public void TryGetEvents_WhenBatchSize1_ShouldReturn1Event()
{
// Arrange
using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel);
// Act
- bool success = reader.TryGetEvents(out var events, batchSize: 10);
+ bool success = reader.TryGetEvents(out var events, batchSize: 1);
// Assert
- Assert.True(success);
- Assert.NotNull(events);
- Assert.True(events.Length <= 10);
+ if (success && events.Length > 0)
+ {
+ Assert.Single(events);
+ }
}
[Fact]
- public void TryGetEvents_WhenBatchSize1_ShouldReturn1Event()
+ public void TryGetEvents_WhenBatchSize10_ShouldReturnUpTo10Events()
{
// Arrange
using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel);
// Act
- bool success = reader.TryGetEvents(out var events, batchSize: 1);
+ bool success = reader.TryGetEvents(out var events, batchSize: 10);
// Assert
- if (success && events.Length > 0)
- {
- Assert.Single(events);
- }
+ Assert.True(success);
+ Assert.NotNull(events);
+ Assert.True(events.Length <= 10);
}
[Fact]
diff --git a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogSessionTests.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogSessionTests.cs
index 6cdd0870..5359c415 100644
--- a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogSessionTests.cs
+++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogSessionTests.cs
@@ -2,8 +2,8 @@
// // Licensed under the MIT License.
using EventLogExpert.Eventing.Common.Channels;
-using EventLogExpert.Eventing.IntegrationTests.TestUtils.Constants;
using EventLogExpert.Eventing.Readers;
+using EventLogExpert.Eventing.TestUtils.Constants;
namespace EventLogExpert.Eventing.IntegrationTests.Readers;
@@ -92,26 +92,6 @@ public void GetLogNames_AfterGetProviderNames_ShouldStillWork()
Assert.NotEmpty(logNames);
}
- [Fact]
- public void GetLogNames_WhenCalledMultipleTimes_ShouldReturnConsistentResults()
- {
- // Arrange
- var session = EventLogSession.GlobalSession;
-
- // Act
- var logNames1 = session.GetLogNames().ToList();
- var logNames2 = session.GetLogNames().ToList();
-
- // Assert
- Assert.Equal(logNames1.Count, logNames2.Count);
-
- // All names from first call should be in second call
- foreach (var name in logNames1)
- {
- Assert.Contains(name, logNames2);
- }
- }
-
[Fact]
public void GetLogNames_WhenCalled_AllNamesShouldBeNonEmpty()
{
@@ -215,6 +195,26 @@ public void GetLogNames_WhenCalled_ShouldReturnOrderedList()
Assert.Equal(sortedNames, logNames);
}
+ [Fact]
+ public void GetLogNames_WhenCalledMultipleTimes_ShouldReturnConsistentResults()
+ {
+ // Arrange
+ var session = EventLogSession.GlobalSession;
+
+ // Act
+ var logNames1 = session.GetLogNames().ToList();
+ var logNames2 = session.GetLogNames().ToList();
+
+ // Assert
+ Assert.Equal(logNames1.Count, logNames2.Count);
+
+ // All names from first call should be in second call
+ foreach (var name in logNames1)
+ {
+ Assert.Contains(name, logNames2);
+ }
+ }
+
[Fact]
public async Task GetLogNames_WhenConcurrentAccess_ShouldHandleMultipleThreads()
{
@@ -254,26 +254,6 @@ public void GetProviderNames_AfterGetLogNames_ShouldStillWork()
Assert.NotEmpty(providers);
}
- [Fact]
- public void GetProviderNames_WhenCalledMultipleTimes_ShouldReturnConsistentResults()
- {
- // Arrange
- var session = EventLogSession.GlobalSession;
-
- // Act
- var providers1 = session.GetProviderNames();
- var providers2 = session.GetProviderNames();
-
- // Assert
- Assert.Equal(providers1.Count, providers2.Count);
-
- // All providers from first call should be in second call
- foreach (var provider in providers1)
- {
- Assert.Contains(provider, providers2);
- }
- }
-
[Fact]
public void GetProviderNames_WhenCalled_AllNamesShouldBeNonEmpty()
{
@@ -349,6 +329,26 @@ public void GetProviderNames_WhenCalled_ShouldReturnProviderNames()
Assert.NotEmpty(providers);
}
+ [Fact]
+ public void GetProviderNames_WhenCalledMultipleTimes_ShouldReturnConsistentResults()
+ {
+ // Arrange
+ var session = EventLogSession.GlobalSession;
+
+ // Act
+ var providers1 = session.GetProviderNames();
+ var providers2 = session.GetProviderNames();
+
+ // Assert
+ Assert.Equal(providers1.Count, providers2.Count);
+
+ // All providers from first call should be in second call
+ foreach (var provider in providers1)
+ {
+ Assert.Contains(provider, providers2);
+ }
+ }
+
[Fact]
public async Task GetProviderNames_WhenConcurrentAccess_ShouldHandleMultipleThreads()
{
@@ -374,26 +374,26 @@ public async Task GetProviderNames_WhenConcurrentAccess_ShouldHandleMultipleThre
}
[Fact]
- public void GlobalSession_WhenAccessedMultipleTimes_ShouldReturnSameInstance()
+ public void GlobalSession_WhenAccessed_ShouldNotBeNull()
{
// Arrange & Act
- var session1 = EventLogSession.GlobalSession;
- var session2 = EventLogSession.GlobalSession;
+ var session = EventLogSession.GlobalSession;
// Assert
- Assert.NotNull(session1);
- Assert.NotNull(session2);
- Assert.Same(session1, session2);
+ Assert.NotNull(session);
}
[Fact]
- public void GlobalSession_WhenAccessed_ShouldNotBeNull()
+ public void GlobalSession_WhenAccessedMultipleTimes_ShouldReturnSameInstance()
{
// Arrange & Act
- var session = EventLogSession.GlobalSession;
+ var session1 = EventLogSession.GlobalSession;
+ var session2 = EventLogSession.GlobalSession;
// Assert
- Assert.NotNull(session);
+ Assert.NotNull(session1);
+ Assert.NotNull(session2);
+ Assert.Same(session1, session2);
}
[Fact]
diff --git a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs
index 2406e1fe..4508b0c5 100644
--- a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs
+++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs
@@ -2,8 +2,8 @@
// // Licensed under the MIT License.
using EventLogExpert.Eventing.Common.Channels;
-using EventLogExpert.Eventing.IntegrationTests.TestUtils.Constants;
using EventLogExpert.Eventing.Readers;
+using EventLogExpert.Eventing.TestUtils.Constants;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Reflection;
@@ -25,18 +25,6 @@ public void Constructor_WithCommonLogs_ShouldCreateWatcher(string logName)
Assert.False(watcher.Enabled);
}
- [Fact]
- public void Constructor_WithDifferentRenderXmlValues_ShouldCreateWatcher()
- {
- // Arrange & Act
- using var watcherWithXml = new EventLogWatcher(Constants.ApplicationLogName, renderXml: true);
- using var watcherWithoutXml = new EventLogWatcher(Constants.ApplicationLogName, renderXml: false);
-
- // Assert
- Assert.NotNull(watcherWithXml);
- Assert.NotNull(watcherWithoutXml);
- }
-
[Fact]
public void Constructor_WithEmptyLogName_ShouldThrowArgumentException()
{
@@ -54,49 +42,6 @@ public void Constructor_WithInvalidLogName_ShouldThrowFileNotFoundException()
Assert.Throws(() => new EventLogWatcher(invalidLogName));
}
- [Fact]
- public void Constructor_WithLogNameAndRenderXml_ShouldCreateWatcher()
- {
- // Arrange & Act
- using var watcher = new EventLogWatcher(Constants.ApplicationLogName, renderXml: true);
-
- // Assert
- Assert.NotNull(watcher);
- }
-
- [Fact]
- public void Constructor_WithLogNameBookmarkAndRenderXml_ShouldCreateWatcher()
- {
- // Arrange
- string? bookmark = null;
-
- // Act
- using var watcher = new EventLogWatcher(Constants.ApplicationLogName, bookmark, renderXml: false);
-
- // Assert
- Assert.NotNull(watcher);
- }
-
- [Fact]
- public void Constructor_WithLogName_ShouldCreateWatcher()
- {
- // Arrange & Act
- using var watcher = new EventLogWatcher(Constants.ApplicationLogName);
-
- // Assert
- Assert.NotNull(watcher);
- }
-
- [Fact]
- public void Constructor_WithNullBookmark_ShouldCreateWatcher()
- {
- // Arrange & Act
- using var watcher = new EventLogWatcher(Constants.ApplicationLogName, null, renderXml: false);
-
- // Assert
- Assert.NotNull(watcher);
- }
-
[Fact]
public void Constructor_WithNullLogName_ShouldThrowArgumentException()
{
@@ -197,6 +142,20 @@ public void Dispose_ShouldReleaseUnderlyingWaitHandle()
Assert.True(newEvents.SafeWaitHandle.IsClosed);
}
+ [Fact]
+ public void Dispose_WhenCalled_ShouldUnsubscribe()
+ {
+ // Arrange
+ var watcher = new EventLogWatcher(Constants.ApplicationLogName);
+ watcher.Enabled = true;
+
+ // Act
+ watcher.Dispose();
+
+ // Assert
+ Assert.False(watcher.Enabled);
+ }
+
[Fact]
public void Dispose_WhenCalledFromHandler_ShouldThrowInvalidOperationException()
{
@@ -209,7 +168,8 @@ public void Dispose_WhenCalledFromHandler_ShouldThrowInvalidOperationException()
{
try
{
- ((EventLogWatcher)sender!).Dispose();
+ Assert.NotNull(sender);
+ ((EventLogWatcher)sender).Dispose();
}
catch (Exception ex)
{
@@ -251,20 +211,6 @@ public void Dispose_WhenCalledMultipleTimes_ShouldNotThrow()
watcher.Dispose();
}
- [Fact]
- public void Dispose_WhenCalled_ShouldUnsubscribe()
- {
- // Arrange
- var watcher = new EventLogWatcher(Constants.ApplicationLogName);
- watcher.Enabled = true;
-
- // Act
- watcher.Dispose();
-
- // Assert
- Assert.False(watcher.Enabled);
- }
-
[Fact]
public void Dispose_WhenNotSubscribed_ShouldNotThrow()
{
@@ -343,6 +289,20 @@ public async Task Enabled_WhenRacingFromMultipleThreads_ShouldNotHangOrCorruptSt
await allDone.WaitAsync(TimeSpan.FromSeconds(30), ct);
}
+ [Fact]
+ public void Enabled_WhenSetToFalse_ShouldUnsubscribe()
+ {
+ // Arrange
+ using var watcher = new EventLogWatcher(Constants.ApplicationLogName);
+ watcher.Enabled = true;
+
+ // Act
+ watcher.Enabled = false;
+
+ // Assert
+ Assert.False(watcher.Enabled);
+ }
+
[Fact]
public void Enabled_WhenSetToFalseFromHandler_ShouldThrowInvalidOperationException()
{
@@ -355,7 +315,8 @@ public void Enabled_WhenSetToFalseFromHandler_ShouldThrowInvalidOperationExcepti
{
try
{
- ((EventLogWatcher)sender!).Enabled = false;
+ Assert.NotNull(sender);
+ ((EventLogWatcher)sender).Enabled = false;
}
catch (Exception ex)
{
@@ -398,29 +359,28 @@ public void Enabled_WhenSetToFalseTwice_ShouldNotThrow()
}
[Fact]
- public void Enabled_WhenSetToFalse_ShouldUnsubscribe()
+ public void Enabled_WhenSetToSameValue_ShouldNotThrow()
{
// Arrange
using var watcher = new EventLogWatcher(Constants.ApplicationLogName);
- watcher.Enabled = true;
-
- // Act
watcher.Enabled = false;
- // Assert
+ // Act & Assert
+ watcher.Enabled = false;
Assert.False(watcher.Enabled);
}
[Fact]
- public void Enabled_WhenSetToSameValue_ShouldNotThrow()
+ public void Enabled_WhenSetToTrue_ShouldSubscribe()
{
// Arrange
using var watcher = new EventLogWatcher(Constants.ApplicationLogName);
- watcher.Enabled = false;
- // Act & Assert
- watcher.Enabled = false;
- Assert.False(watcher.Enabled);
+ // Act
+ watcher.Enabled = true;
+
+ // Assert
+ Assert.True(watcher.Enabled);
}
[Fact]
@@ -462,19 +422,6 @@ public void Enabled_WhenSetToTrueTwice_ShouldRemainEnabled()
Assert.True(watcher.Enabled);
}
- [Fact]
- public void Enabled_WhenSetToTrue_ShouldSubscribe()
- {
- // Arrange
- using var watcher = new EventLogWatcher(Constants.ApplicationLogName);
-
- // Act
- watcher.Enabled = true;
-
- // Assert
- Assert.True(watcher.Enabled);
- }
-
[Fact]
public void Enabled_WhenToggled_ShouldUpdateState()
{
diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventProviderDatabaseEventResolverTests.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Resolvers/EventProviderDatabaseEventResolverTests.cs
similarity index 89%
rename from tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventProviderDatabaseEventResolverTests.cs
rename to tests/Integration/EventLogExpert.Eventing.IntegrationTests/Resolvers/EventProviderDatabaseEventResolverTests.cs
index 445f233c..f5b20213 100644
--- a/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventProviderDatabaseEventResolverTests.cs
+++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Resolvers/EventProviderDatabaseEventResolverTests.cs
@@ -6,33 +6,16 @@
using EventLogExpert.Eventing.ProviderDatabase;
using EventLogExpert.Eventing.Readers;
using EventLogExpert.Eventing.Resolvers;
-using EventLogExpert.Eventing.Tests.TestUtils;
-using EventLogExpert.Eventing.Tests.TestUtils.Constants;
-using Microsoft.Data.Sqlite;
+using EventLogExpert.Eventing.TestUtils;
+using EventLogExpert.Eventing.TestUtils.Constants;
using NSubstitute;
using System.Collections.Concurrent;
using System.Collections.Immutable;
-namespace EventLogExpert.Eventing.Tests.Resolvers;
+namespace EventLogExpert.Eventing.IntegrationTests.Resolvers;
public sealed class EventResolverDatabaseTests
{
- [Fact]
- public void Constructor_WithCacheAndLogger_ShouldCreateInstance()
- {
- // Arrange
- var dbCollection = Substitute.For();
- dbCollection.ActiveDatabases.Returns([]);
- var cache = Substitute.For();
- var logger = Substitute.For();
-
- // Act
- using var resolver = new EventResolver(dbCollection, cache, logger);
-
- // Assert
- Assert.NotNull(resolver);
- }
-
[Fact]
public void Constructor_WithLogger_ShouldLogDatabasePaths()
{
@@ -88,44 +71,6 @@ public void Constructor_WithNonExistentDatabase_ShouldThrowFileNotFoundException
Assert.Contains(nonExistentPath, exception.Message);
}
- [Fact]
- public void Constructor_WithNullCache_ShouldCreateInstance()
- {
- // Arrange
- var dbCollection = Substitute.For();
- dbCollection.ActiveDatabases.Returns([]);
-
- // Act
- using var resolver = new EventResolver(dbCollection, null);
-
- // Assert
- Assert.NotNull(resolver);
- }
-
- [Fact]
- public void Constructor_WithNullDatabaseCollection_ShouldCreateInstance()
- {
- // Act
- using var resolver = new EventResolver(null);
-
- // Assert
- Assert.NotNull(resolver);
- }
-
- [Fact]
- public void Constructor_WithNullLogger_ShouldCreateInstance()
- {
- // Arrange
- var dbCollection = Substitute.For();
- dbCollection.ActiveDatabases.Returns([]);
-
- // Act
- using var resolver = new EventResolver(dbCollection, logger: null);
-
- // Assert
- Assert.NotNull(resolver);
- }
-
[Fact]
public void Constructor_WithValidDatabase_ShouldLoadDatabase()
{
@@ -283,40 +228,40 @@ public void Dispose_ThenLoadProviderDetails_ShouldThrowObjectDisposedException()
}
[Fact]
- public void Dispose_ThenResolveEventViaBaseReference_ShouldThrowObjectDisposedException()
+ public void Dispose_ThenResolveEvent_ShouldThrowObjectDisposedException()
{
// Arrange
var dbCollection = Substitute.For();
dbCollection.ActiveDatabases.Returns([]);
- EventResolver resolver = new EventResolver(dbCollection);
-
- // Type as base class to verify override (not 'new') is used
- EventResolverBase baseResolver = resolver;
+ var resolver = new EventResolver(dbCollection);
var eventRecord = EventUtils.CreateBasicEvent();
// Act
resolver.Dispose();
- // Assert - This should throw because ResolveEvent is overridden, not hidden with 'new'
- Assert.Throws(() => baseResolver.ResolveEvent(eventRecord));
+ // Assert
+ Assert.Throws(() => resolver.ResolveEvent(eventRecord));
}
[Fact]
- public void Dispose_ThenResolveEvent_ShouldThrowObjectDisposedException()
+ public void Dispose_ThenResolveEventViaBaseReference_ShouldThrowObjectDisposedException()
{
// Arrange
var dbCollection = Substitute.For();
dbCollection.ActiveDatabases.Returns([]);
- var resolver = new EventResolver(dbCollection);
+ EventResolver resolver = new(dbCollection);
+
+ // Type as base class to verify override (not 'new') is used
+ EventResolverBase baseResolver = resolver;
var eventRecord = EventUtils.CreateBasicEvent();
// Act
resolver.Dispose();
- // Assert
- Assert.Throws(() => resolver.ResolveEvent(eventRecord));
+ // Assert - This should throw because ResolveEvent is overridden, not hidden with 'new'
+ Assert.Throws(() => baseResolver.ResolveEvent(eventRecord));
}
[Fact]
@@ -724,33 +669,5 @@ public void LoadProviderDetails_WithUnknownProvider_ShouldAddEmptyDetails()
Assert.Null(exception);
}
- private static void DeleteDatabaseFile(string path, int maxRetries = 10)
- {
- if (!File.Exists(path)) { return; }
-
- for (int i = 0; i < maxRetries; i++)
- {
- try
- {
- // Clear SQLite connection pool
- SqliteConnection.ClearAllPools();
-
- GC.Collect();
- GC.WaitForPendingFinalizers();
- GC.Collect();
-
- File.Delete(path);
- return;
- }
- catch (IOException) when (i < maxRetries - 1)
- {
- Thread.Sleep(200);
- }
- catch (IOException)
- {
- // If we still can't delete after retries, just ignore - OS will clean up temp files
- return;
- }
- }
- }
+ private static void DeleteDatabaseFile(string path) => SqliteTestDb.Delete(path);
}
diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/LocalProviderEventResolverTests.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Resolvers/LocalProviderEventResolverTests.cs
similarity index 98%
rename from tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/LocalProviderEventResolverTests.cs
rename to tests/Integration/EventLogExpert.Eventing.IntegrationTests/Resolvers/LocalProviderEventResolverTests.cs
index 47c09cc1..2953c0e7 100644
--- a/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/LocalProviderEventResolverTests.cs
+++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Resolvers/LocalProviderEventResolverTests.cs
@@ -4,12 +4,12 @@
using EventLogExpert.Eventing.Logging;
using EventLogExpert.Eventing.Readers;
using EventLogExpert.Eventing.Resolvers;
-using EventLogExpert.Eventing.Tests.TestUtils;
-using EventLogExpert.Eventing.Tests.TestUtils.Constants;
+using EventLogExpert.Eventing.TestUtils;
+using EventLogExpert.Eventing.TestUtils.Constants;
using NSubstitute;
using System.Collections.Concurrent;
-namespace EventLogExpert.Eventing.Tests.Resolvers;
+namespace EventLogExpert.Eventing.IntegrationTests.Resolvers;
public sealed class EventResolverLocalProviderTests
{
@@ -155,19 +155,32 @@ public void LoadProviderDetails_CalledTwiceForSameProvider_ShouldNotThrow()
}
[Fact]
- public void LoadProviderDetails_ConcurrentCallsForSameProvider_ShouldHandleThreadSafely()
+ public void LoadProviderDetails_ConcurrentCalls_ShouldHandleThreadSafely()
{
// Arrange
var resolver = new EventResolver();
- var exceptions = new Exception?[50];
+
+ var providerNames = new[]
+ {
+ Constants.ApplicationLogName,
+ Constants.SystemLogName,
+ Constants.TestProviderName,
+ Constants.TestProviderLongName
+ };
+
+ var exceptions = new Exception?[20];
// Act
- Parallel.For(0, 50, i =>
+ Parallel.For(0, 20, i =>
{
try
{
- var eventRecord = EventUtils.CreateBasicEvent();
- eventRecord.Id = (ushort)(1000 + i);
+ var eventRecord = new EventRecord
+ {
+ ProviderName = providerNames[i % providerNames.Length],
+ Id = (ushort)(1000 + i)
+ };
+
resolver.LoadProviderDetails(eventRecord);
}
catch (Exception ex)
@@ -181,32 +194,19 @@ public void LoadProviderDetails_ConcurrentCallsForSameProvider_ShouldHandleThrea
}
[Fact]
- public void LoadProviderDetails_ConcurrentCalls_ShouldHandleThreadSafely()
+ public void LoadProviderDetails_ConcurrentCallsForSameProvider_ShouldHandleThreadSafely()
{
// Arrange
var resolver = new EventResolver();
-
- var providerNames = new[]
- {
- Constants.ApplicationLogName,
- Constants.SystemLogName,
- Constants.TestProviderName,
- Constants.TestProviderLongName
- };
-
- var exceptions = new Exception?[20];
+ var exceptions = new Exception?[50];
// Act
- Parallel.For(0, 20, i =>
+ Parallel.For(0, 50, i =>
{
try
{
- var eventRecord = new EventRecord
- {
- ProviderName = providerNames[i % providerNames.Length],
- Id = (ushort)(1000 + i)
- };
-
+ var eventRecord = EventUtils.CreateBasicEvent();
+ eventRecord.Id = (ushort)(1000 + i);
resolver.LoadProviderDetails(eventRecord);
}
catch (Exception ex)
diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/VersatileEventResolverTests.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Resolvers/VersatileEventResolverTests.cs
similarity index 87%
rename from tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/VersatileEventResolverTests.cs
rename to tests/Integration/EventLogExpert.Eventing.IntegrationTests/Resolvers/VersatileEventResolverTests.cs
index 80919016..57f086be 100644
--- a/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/VersatileEventResolverTests.cs
+++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Resolvers/VersatileEventResolverTests.cs
@@ -8,30 +8,15 @@
using EventLogExpert.Eventing.Providers;
using EventLogExpert.Eventing.Readers;
using EventLogExpert.Eventing.Resolvers;
-using EventLogExpert.Eventing.Tests.TestUtils;
-using EventLogExpert.Eventing.Tests.TestUtils.Constants;
-using Microsoft.Data.Sqlite;
+using EventLogExpert.Eventing.TestUtils;
+using EventLogExpert.Eventing.TestUtils.Constants;
using NSubstitute;
using System.Collections.Immutable;
-namespace EventLogExpert.Eventing.Tests.Resolvers;
+namespace EventLogExpert.Eventing.IntegrationTests.Resolvers;
public sealed class EventResolverTests
{
- [Fact]
- public void Constructor_WithCacheAndLogger_ShouldCreateInstance()
- {
- // Arrange
- var cache = new EventResolverCache();
- var logger = Substitute.For();
-
- // Act
- using var resolver = new EventResolver(cache: cache, logger: logger);
-
- // Assert
- Assert.NotNull(resolver);
- }
-
[Fact]
public void Constructor_WithDatabaseCollection_ShouldUseDatabaseResolver()
{
@@ -56,13 +41,7 @@ public void Constructor_WithDatabaseCollection_ShouldUseDatabaseResolver()
}
finally
{
- if (File.Exists(dbPath))
- {
- SqliteConnection.ClearAllPools();
- GC.Collect();
- GC.WaitForPendingFinalizers();
- File.Delete(dbPath);
- }
+ SqliteTestDb.Delete(dbPath);
}
}
@@ -93,16 +72,6 @@ public void Constructor_WithLogger_ShouldLogInstantiation()
logger.Received().Debug(Arg.Is(h => h.ToString().Contains("EventResolver")));
}
- [Fact]
- public void Constructor_WithNoDatabaseCollection_ShouldUseLocalResolver()
- {
- // Act
- using var resolver = new EventResolver(null);
-
- // Assert
- Assert.NotNull(resolver);
- }
-
[Fact]
public void Dispose_CalledMultipleTimes_ShouldNotThrow()
{
@@ -146,13 +115,7 @@ public void Dispose_ThenLoadProviderDetails_WithDatabaseResolver_ShouldThrowObje
}
finally
{
- if (File.Exists(dbPath))
- {
- SqliteConnection.ClearAllPools();
- GC.Collect();
- GC.WaitForPendingFinalizers();
- File.Delete(dbPath);
- }
+ SqliteTestDb.Delete(dbPath);
}
}
@@ -201,13 +164,7 @@ public void Dispose_ThenResolveEvent_WithDatabaseResolver_ShouldThrowObjectDispo
}
finally
{
- if (File.Exists(dbPath))
- {
- SqliteConnection.ClearAllPools();
- GC.Collect();
- GC.WaitForPendingFinalizers();
- File.Delete(dbPath);
- }
+ SqliteTestDb.Delete(dbPath);
}
}
@@ -309,13 +266,7 @@ public void LoadProviderDetails_WithDatabaseResolver_ShouldResolve()
}
finally
{
- if (File.Exists(dbPath))
- {
- SqliteConnection.ClearAllPools();
- GC.Collect();
- GC.WaitForPendingFinalizers();
- File.Delete(dbPath);
- }
+ SqliteTestDb.Delete(dbPath);
}
}
@@ -433,13 +384,7 @@ public void ResolveEvent_WithDatabaseResolver_ShouldResolveEvent()
}
finally
{
- if (File.Exists(dbPath))
- {
- SqliteConnection.ClearAllPools();
- GC.Collect();
- GC.WaitForPendingFinalizers();
- File.Delete(dbPath);
- }
+ SqliteTestDb.Delete(dbPath);
}
}
@@ -532,13 +477,7 @@ public void ResolveEvent_WithMixedDbAndUnknownProviders_ShouldOnlyApplyDatabaseT
}
finally
{
- if (File.Exists(dbPath))
- {
- SqliteConnection.ClearAllPools();
- GC.Collect();
- GC.WaitForPendingFinalizers();
- File.Delete(dbPath);
- }
+ SqliteTestDb.Delete(dbPath);
}
}
@@ -631,13 +570,7 @@ public void SwitchBetweenResolvers_WithDifferentInstances_ShouldWorkIndependentl
}
finally
{
- if (File.Exists(dbPath))
- {
- SqliteConnection.ClearAllPools();
- GC.Collect();
- GC.WaitForPendingFinalizers();
- File.Delete(dbPath);
- }
+ SqliteTestDb.Delete(dbPath);
}
}
}
diff --git a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/TestUtils/Constants/Constants.Provider.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/TestUtils/Constants/Constants.Provider.cs
deleted file mode 100644
index 91b986c1..00000000
--- a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/TestUtils/Constants/Constants.Provider.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-// // Copyright (c) Microsoft Corporation.
-// // Licensed under the MIT License.
-
-namespace EventLogExpert.Eventing.IntegrationTests.TestUtils.Constants;
-
-public sealed partial class Constants
-{
- public const string ApplicationLogName = "Application";
-
- public const string KernelGeneralLogName = "Microsoft-Windows-Kernel-General";
- public const string PowerShellLogName = "Microsoft-Windows-PowerShell";
- public const string SecurityAuditingLogName = "Microsoft-Windows-Security-Auditing";
- public const string SecurityLogName = "Security";
- public const string ServiceControlManagerLogName = "Service Control Manager";
- public const string SystemLogName = "System";
-
- public const string TestProviderName = "TestProvider";
-}
diff --git a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/xunit.runner.json b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/xunit.runner.json
new file mode 100644
index 00000000..77778efd
--- /dev/null
+++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/xunit.runner.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://xunit.net/schema/v3/xunit.runner.schema.json",
+ "parallelizeAssembly": false,
+ "parallelizeTestCollections": false
+}
diff --git a/tests/Unit/EventLogExpert.UI.Tests/Database/DatabaseServiceTests.cs b/tests/Integration/EventLogExpert.UI.IntegrationTests/Database/DatabaseServiceTests.cs
similarity index 99%
rename from tests/Unit/EventLogExpert.UI.Tests/Database/DatabaseServiceTests.cs
rename to tests/Integration/EventLogExpert.UI.IntegrationTests/Database/DatabaseServiceTests.cs
index 5fcf62f8..9a355afa 100644
--- a/tests/Unit/EventLogExpert.UI.Tests/Database/DatabaseServiceTests.cs
+++ b/tests/Integration/EventLogExpert.UI.IntegrationTests/Database/DatabaseServiceTests.cs
@@ -6,13 +6,13 @@
using EventLogExpert.UI.Common.Preferences;
using EventLogExpert.UI.Database;
using EventLogExpert.UI.Database.Upgrade;
-using EventLogExpert.UI.Tests.TestUtils;
-using EventLogExpert.UI.Tests.TestUtils.Constants;
+using EventLogExpert.UI.IntegrationTests.TestUtils;
+using EventLogExpert.UI.IntegrationTests.TestUtils.Constants;
using Microsoft.Data.Sqlite;
using NSubstitute;
using System.IO.Compression;
-namespace EventLogExpert.UI.Tests.Database;
+namespace EventLogExpert.UI.IntegrationTests.Database;
public sealed class DatabaseServiceTests : IDisposable
{
@@ -258,51 +258,48 @@ public async Task ClassifyEntriesAsync_WhenV3BakAppearsBetweenClassifications_Sh
}
[Fact]
- public async Task ClassifyEntriesAsync_WhenV3SchemaWithUpgradeBak_ShouldDetectAsUpgradeRequiredAndBackupExistsTrue()
+ public async Task ClassifyEntriesAsync_WhenV3Schema_ShouldDetectAsUpgradeRequired()
{
var databasePath = CreateDatabaseDirectory();
var dbPath = Path.Combine(databasePath, Constants.TestDb1);
DatabaseSeedUtils.SeedV3Schema(dbPath);
- var bakPath = dbPath + DatabaseService.UpgradeBackupSuffix;
- File.WriteAllText(bakPath, "interrupted-upgrade-backup");
-
var service = CreateDatabaseService();
await service.ClassifyEntriesAsync(TestContext.Current.CancellationToken);
var entry = Assert.Single(service.Entries);
Assert.Equal(DatabaseStatus.UpgradeRequired, entry.Status);
- Assert.True(entry.BackupExists);
- Assert.True(File.Exists(bakPath), ".upgrade.bak must be preserved for V3 entries so recovery can restore it.");
+ Assert.False(entry.BackupExists);
}
[Fact]
- public async Task ClassifyEntriesAsync_WhenV3Schema_ShouldDetectAsUpgradeRequired()
+ public async Task ClassifyEntriesAsync_WhenV3SchemaWithUpgradeBak_ShouldDetectAsUpgradeRequiredAndBackupExistsTrue()
{
var databasePath = CreateDatabaseDirectory();
var dbPath = Path.Combine(databasePath, Constants.TestDb1);
DatabaseSeedUtils.SeedV3Schema(dbPath);
+ var bakPath = dbPath + DatabaseService.UpgradeBackupSuffix;
+ File.WriteAllText(bakPath, "interrupted-upgrade-backup");
+
var service = CreateDatabaseService();
await service.ClassifyEntriesAsync(TestContext.Current.CancellationToken);
var entry = Assert.Single(service.Entries);
Assert.Equal(DatabaseStatus.UpgradeRequired, entry.Status);
- Assert.False(entry.BackupExists);
+ Assert.True(entry.BackupExists);
+ Assert.True(File.Exists(bakPath), ".upgrade.bak must be preserved for V3 entries so recovery can restore it.");
}
[Fact]
- public async Task ClassifyEntriesAsync_WhenV4SchemaWithUpgradeBak_ShouldDeleteBakAndDetectAsReady()
+ public async Task ClassifyEntriesAsync_WhenV4Schema_ShouldDetectAsReady()
{
var databasePath = CreateDatabaseDirectory();
var dbPath = Path.Combine(databasePath, Constants.TestDb1);
DatabaseSeedUtils.SeedV4Schema(dbPath);
- var bakPath = dbPath + DatabaseService.UpgradeBackupSuffix;
- File.WriteAllText(bakPath, "stale-backup-from-successful-upgrade");
-
var service = CreateDatabaseService();
await service.ClassifyEntriesAsync(TestContext.Current.CancellationToken);
@@ -310,16 +307,18 @@ public async Task ClassifyEntriesAsync_WhenV4SchemaWithUpgradeBak_ShouldDeleteBa
var entry = Assert.Single(service.Entries);
Assert.Equal(DatabaseStatus.Ready, entry.Status);
Assert.False(entry.BackupExists);
- Assert.False(File.Exists(bakPath), "Stale .upgrade.bak must be cleaned up once the main file reaches V4.");
}
[Fact]
- public async Task ClassifyEntriesAsync_WhenV4Schema_ShouldDetectAsReady()
+ public async Task ClassifyEntriesAsync_WhenV4SchemaWithUpgradeBak_ShouldDeleteBakAndDetectAsReady()
{
var databasePath = CreateDatabaseDirectory();
var dbPath = Path.Combine(databasePath, Constants.TestDb1);
DatabaseSeedUtils.SeedV4Schema(dbPath);
+ var bakPath = dbPath + DatabaseService.UpgradeBackupSuffix;
+ File.WriteAllText(bakPath, "stale-backup-from-successful-upgrade");
+
var service = CreateDatabaseService();
await service.ClassifyEntriesAsync(TestContext.Current.CancellationToken);
@@ -327,6 +326,7 @@ public async Task ClassifyEntriesAsync_WhenV4Schema_ShouldDetectAsReady()
var entry = Assert.Single(service.Entries);
Assert.Equal(DatabaseStatus.Ready, entry.Status);
Assert.False(entry.BackupExists);
+ Assert.False(File.Exists(bakPath), "Stale .upgrade.bak must be cleaned up once the main file reaches V4.");
}
[Fact]
@@ -647,25 +647,6 @@ public async Task DisposeAsync_WithPendingBatches_ShouldCancelInFlightAndPending
Assert.Equal(Constants.TestDb2, pendingResult.Cancelled[0]);
}
- [Fact]
- public void EntriesChanged_MultipleSubscribers_FirstThrows_ShouldStillInvokeRest()
- {
- var databasePath = CreateDatabaseDirectory();
- CreateDatabaseFile(databasePath, Constants.TestDb1);
-
- var service = CreateDatabaseService();
-
- var secondSubscriberInvocations = 0;
-
- service.EntriesChanged += (_, _) => throw new InvalidOperationException("first subscriber throws");
- service.EntriesChanged += (_, _) => Interlocked.Increment(ref secondSubscriberInvocations);
-
- service.Toggle(Constants.TestDb1);
-
- // If multicast invoke aborted on the first throwing subscriber, this would be 0.
- Assert.Equal(1, secondSubscriberInvocations);
- }
-
[Fact]
public void Entries_WhenMixedVersionedAndNonVersioned_ShouldSortCorrectly()
{
@@ -726,6 +707,25 @@ public void Entries_WhenSimpleNames_ShouldSortByNameAscThenVersionDesc()
Assert.Equal(Constants.DatabaseA + ".db", service.Entries[2].FileName);
}
+ [Fact]
+ public void EntriesChanged_MultipleSubscribers_FirstThrows_ShouldStillInvokeRest()
+ {
+ var databasePath = CreateDatabaseDirectory();
+ CreateDatabaseFile(databasePath, Constants.TestDb1);
+
+ var service = CreateDatabaseService();
+
+ var secondSubscriberInvocations = 0;
+
+ service.EntriesChanged += (_, _) => throw new InvalidOperationException("first subscriber throws");
+ service.EntriesChanged += (_, _) => Interlocked.Increment(ref secondSubscriberInvocations);
+
+ service.Toggle(Constants.TestDb1);
+
+ // If multicast invoke aborted on the first throwing subscriber, this would be 0.
+ Assert.Equal(1, secondSubscriberInvocations);
+ }
+
[Fact]
public async Task EnumerateZipDbEntryNamesAsync_MalformedZip_ShouldReturnEmpty_NotThrow()
{
@@ -2144,7 +2144,7 @@ public async Task UpgradeBatchAsync_DuringMigration_ShouldNotSetBackupExistsOnEn
Assert.Single(result.Succeeded);
Assert.NotNull(backupExistsDuringMigration);
- Assert.False(backupExistsDuringMigration!.Value);
+ Assert.False(backupExistsDuringMigration.Value);
}
[Fact]
diff --git a/tests/Unit/EventLogExpert.UI.Tests/DebugLog/DebugLogServiceTests.cs b/tests/Integration/EventLogExpert.UI.IntegrationTests/DebugLog/DebugLogServiceTests.cs
similarity index 99%
rename from tests/Unit/EventLogExpert.UI.Tests/DebugLog/DebugLogServiceTests.cs
rename to tests/Integration/EventLogExpert.UI.IntegrationTests/DebugLog/DebugLogServiceTests.cs
index db8e9a24..0123601c 100644
--- a/tests/Unit/EventLogExpert.UI.Tests/DebugLog/DebugLogServiceTests.cs
+++ b/tests/Integration/EventLogExpert.UI.IntegrationTests/DebugLog/DebugLogServiceTests.cs
@@ -3,12 +3,12 @@
using EventLogExpert.UI.Common.Files;
using EventLogExpert.UI.DebugLog;
+using EventLogExpert.UI.IntegrationTests.TestUtils.Constants;
using EventLogExpert.UI.Settings;
-using EventLogExpert.UI.Tests.TestUtils.Constants;
using Microsoft.Extensions.Logging;
using NSubstitute;
-namespace EventLogExpert.UI.Tests.DebugLog;
+namespace EventLogExpert.UI.IntegrationTests.DebugLog;
public sealed class DebugLogServiceTests : IDisposable
{
@@ -330,37 +330,6 @@ public void MinimumLevel_WhenLogLevelChangedAtRuntime_ShouldReflectNewLevel()
Assert.Equal(LogLevel.Warning, debugLogService.MinimumLevel);
}
- [Fact]
- public void TraceIfEnabled_WhenLogLevelChangedAtRuntime_ShouldRespectNewLevel()
- {
- // Arrange
- var (mockSettingsService, setLogLevel) = CreateMockSettingsServiceWithDynamicLogLevel(LogLevel.Debug);
-
- using var debugLogService = new DebugLogService(
- new FileLocationOptions(_testDirectory),
- mockSettingsService);
-
- // Act - write at Debug (should succeed)
- debugLogService.Debug($"debug message before change");
- var contentBefore = ReadLogFile();
- Assert.Contains("debug message before change", contentBefore);
-
- // Change level to Warning
- setLogLevel(LogLevel.Warning);
-
- // Write at Debug (should be filtered by handler)
- debugLogService.Debug($"debug message after change");
-
- // Write at Warning (should succeed)
- debugLogService.Warning($"warning message after change");
-
- // Assert
- var content = ReadLogFile();
- Assert.Contains("debug message before change", content);
- Assert.DoesNotContain("debug message after change", content);
- Assert.Contains("warning message after change", content);
- }
-
[Fact]
public void Trace_ShouldIncludeThreadId()
{
@@ -448,7 +417,8 @@ public async Task Trace_WhenCalledFromMultipleThreadsConcurrently_ShouldProduceO
DebugLogEntryParser.TryParseLine(line, out var entry),
$"Line did not parse via DebugLogEntryParser: {line}");
- timestamps.Add(entry.Timestamp!.Value);
+ Assert.NotNull(entry.Timestamp);
+ timestamps.Add(entry.Timestamp.Value);
payloads.Add(entry.Message);
}
@@ -614,6 +584,37 @@ public void Trace_WithDefaultLogLevel_ShouldUseInformation()
Assert.Contains(Constants.DebugLogDefaultLevelMessage, content);
}
+ [Fact]
+ public void TraceIfEnabled_WhenLogLevelChangedAtRuntime_ShouldRespectNewLevel()
+ {
+ // Arrange
+ var (mockSettingsService, setLogLevel) = CreateMockSettingsServiceWithDynamicLogLevel(LogLevel.Debug);
+
+ using var debugLogService = new DebugLogService(
+ new FileLocationOptions(_testDirectory),
+ mockSettingsService);
+
+ // Act - write at Debug (should succeed)
+ debugLogService.Debug($"debug message before change");
+ var contentBefore = ReadLogFile();
+ Assert.Contains("debug message before change", contentBefore);
+
+ // Change level to Warning
+ setLogLevel(LogLevel.Warning);
+
+ // Write at Debug (should be filtered by handler)
+ debugLogService.Debug($"debug message after change");
+
+ // Write at Warning (should succeed)
+ debugLogService.Warning($"warning message after change");
+
+ // Assert
+ var content = ReadLogFile();
+ Assert.Contains("debug message before change", content);
+ Assert.DoesNotContain("debug message after change", content);
+ Assert.Contains("warning message after change", content);
+ }
+
[Fact]
public void WriteTrace_WhenSecondInstanceWritesToSameFile_ShouldNotThrowFileLockException()
{
diff --git a/tests/Integration/EventLogExpert.UI.IntegrationTests/EventLogExpert.UI.IntegrationTests.csproj b/tests/Integration/EventLogExpert.UI.IntegrationTests/EventLogExpert.UI.IntegrationTests.csproj
new file mode 100644
index 00000000..669ad790
--- /dev/null
+++ b/tests/Integration/EventLogExpert.UI.IntegrationTests/EventLogExpert.UI.IntegrationTests.csproj
@@ -0,0 +1,23 @@
+
+
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Integration/EventLogExpert.UI.IntegrationTests/GlobalUsings.cs b/tests/Integration/EventLogExpert.UI.IntegrationTests/GlobalUsings.cs
new file mode 100644
index 00000000..b04ace3e
--- /dev/null
+++ b/tests/Integration/EventLogExpert.UI.IntegrationTests/GlobalUsings.cs
@@ -0,0 +1,4 @@
+// // Copyright (c) Microsoft Corporation.
+// // Licensed under the MIT License.
+
+global using Xunit;
diff --git a/tests/Integration/EventLogExpert.UI.IntegrationTests/TestUtils/Constants/Constants.Database.cs b/tests/Integration/EventLogExpert.UI.IntegrationTests/TestUtils/Constants/Constants.Database.cs
new file mode 100644
index 00000000..09655316
--- /dev/null
+++ b/tests/Integration/EventLogExpert.UI.IntegrationTests/TestUtils/Constants/Constants.Database.cs
@@ -0,0 +1,27 @@
+// // Copyright (c) Microsoft Corporation.
+// // Licensed under the MIT License.
+
+namespace EventLogExpert.UI.IntegrationTests.TestUtils.Constants;
+
+public sealed partial class Constants
+{
+ // Simple database names (sort: alpha asc)
+ public const string AnotherDb = "AnotherDb";
+ public const string DatabaseA = "Database A";
+ public const string DatabaseB = "Database B";
+ public const string DatabaseC = "Database C";
+
+ // Versioned database names (sort: numeric desc)
+ public const string Server1 = "Server 1";
+ public const string Server10 = "Server 10";
+ public const string Server2 = "Server 2";
+ public const string Server20 = "Server 20";
+ public const string SimpleDatabase = "SimpleDatabase";
+ public const string Windows10 = "Windows 10";
+ public const string Windows11 = "Windows 11";
+
+ // Test database file names
+ public const string TestDb1 = "TestDb1.db";
+ public const string TestDb2 = "TestDb2.db";
+ public const string TestDb3 = "TestDb3.db";
+}
diff --git a/tests/Integration/EventLogExpert.UI.IntegrationTests/TestUtils/Constants/Constants.DebugLog.cs b/tests/Integration/EventLogExpert.UI.IntegrationTests/TestUtils/Constants/Constants.DebugLog.cs
new file mode 100644
index 00000000..ec031bd9
--- /dev/null
+++ b/tests/Integration/EventLogExpert.UI.IntegrationTests/TestUtils/Constants/Constants.DebugLog.cs
@@ -0,0 +1,19 @@
+// // Copyright (c) Microsoft Corporation.
+// // Licensed under the MIT License.
+
+namespace EventLogExpert.UI.IntegrationTests.TestUtils.Constants;
+
+public sealed partial class Constants
+{
+ public const string DebugLogDefaultLevelMessage = "Default level message";
+ public const string DebugLogErrorMessage = "Error message";
+ public const string DebugLogExistingContent = "Existing log content";
+ public const string DebugLogFirstMessage = "First message";
+ public const string DebugLogLine1 = "Line 1";
+ public const string DebugLogLine2 = "Line 2";
+ public const string DebugLogLine3 = "Line 3";
+ public const string DebugLogNewMessage = "New message";
+ public const string DebugLogSecondMessage = "Second message";
+ public const string DebugLogTestMessage = "Test message";
+ public const string DebugLogThirdMessage = "Third message";
+}
diff --git a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/DatabaseSeedUtils.cs b/tests/Integration/EventLogExpert.UI.IntegrationTests/TestUtils/DatabaseSeedUtils.cs
similarity index 98%
rename from tests/Unit/EventLogExpert.UI.Tests/TestUtils/DatabaseSeedUtils.cs
rename to tests/Integration/EventLogExpert.UI.IntegrationTests/TestUtils/DatabaseSeedUtils.cs
index eee2bfeb..00875111 100644
--- a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/DatabaseSeedUtils.cs
+++ b/tests/Integration/EventLogExpert.UI.IntegrationTests/TestUtils/DatabaseSeedUtils.cs
@@ -5,7 +5,7 @@
using EventLogExpert.Eventing.ProviderDatabase;
using Microsoft.Data.Sqlite;
-namespace EventLogExpert.UI.Tests.TestUtils;
+namespace EventLogExpert.UI.IntegrationTests.TestUtils;
internal static class DatabaseSeedUtils
{
diff --git a/tests/Integration/EventLogExpert.UI.IntegrationTests/xunit.runner.json b/tests/Integration/EventLogExpert.UI.IntegrationTests/xunit.runner.json
new file mode 100644
index 00000000..77778efd
--- /dev/null
+++ b/tests/Integration/EventLogExpert.UI.IntegrationTests/xunit.runner.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://xunit.net/schema/v3/xunit.runner.schema.json",
+ "parallelizeAssembly": false,
+ "parallelizeTestCollections": false
+}
diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/CompressionTestUtils.cs b/tests/Shared/EventLogExpert.Eventing.TestUtils/CompressionTestUtils.cs
similarity index 97%
rename from tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/CompressionTestUtils.cs
rename to tests/Shared/EventLogExpert.Eventing.TestUtils/CompressionTestUtils.cs
index 61280e1e..ff5bca71 100644
--- a/tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/CompressionTestUtils.cs
+++ b/tests/Shared/EventLogExpert.Eventing.TestUtils/CompressionTestUtils.cs
@@ -4,7 +4,7 @@
using System.IO.Compression;
using System.Text;
-namespace EventLogExpert.Eventing.Tests.TestUtils;
+namespace EventLogExpert.Eventing.TestUtils;
public static class CompressionTestUtils
{
diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/Constants/Constants.Provider.cs b/tests/Shared/EventLogExpert.Eventing.TestUtils/Constants/Constants.Provider.cs
similarity index 96%
rename from tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/Constants/Constants.Provider.cs
rename to tests/Shared/EventLogExpert.Eventing.TestUtils/Constants/Constants.Provider.cs
index 08819b05..1cd11ec3 100644
--- a/tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/Constants/Constants.Provider.cs
+++ b/tests/Shared/EventLogExpert.Eventing.TestUtils/Constants/Constants.Provider.cs
@@ -1,7 +1,7 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.
-namespace EventLogExpert.Eventing.Tests.TestUtils.Constants;
+namespace EventLogExpert.Eventing.TestUtils.Constants;
public sealed partial class Constants
{
diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/Constants/Constants.Resolver.cs b/tests/Shared/EventLogExpert.Eventing.TestUtils/Constants/Constants.Resolver.cs
similarity index 95%
rename from tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/Constants/Constants.Resolver.cs
rename to tests/Shared/EventLogExpert.Eventing.TestUtils/Constants/Constants.Resolver.cs
index 7840004b..ca9ce627 100644
--- a/tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/Constants/Constants.Resolver.cs
+++ b/tests/Shared/EventLogExpert.Eventing.TestUtils/Constants/Constants.Resolver.cs
@@ -1,7 +1,7 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.
-namespace EventLogExpert.Eventing.Tests.TestUtils.Constants;
+namespace EventLogExpert.Eventing.TestUtils.Constants;
public sealed partial class Constants
{
diff --git a/tests/Shared/EventLogExpert.Eventing.TestUtils/EventLogExpert.Eventing.TestUtils.csproj b/tests/Shared/EventLogExpert.Eventing.TestUtils/EventLogExpert.Eventing.TestUtils.csproj
new file mode 100644
index 00000000..eacb8d28
--- /dev/null
+++ b/tests/Shared/EventLogExpert.Eventing.TestUtils/EventLogExpert.Eventing.TestUtils.csproj
@@ -0,0 +1,11 @@
+
+
+
+ false
+
+
+
+
+
+
+
diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/EventUtils.cs b/tests/Shared/EventLogExpert.Eventing.TestUtils/EventUtils.cs
similarity index 97%
rename from tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/EventUtils.cs
rename to tests/Shared/EventLogExpert.Eventing.TestUtils/EventUtils.cs
index 88cfd6b2..a28a7ee7 100644
--- a/tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/EventUtils.cs
+++ b/tests/Shared/EventLogExpert.Eventing.TestUtils/EventUtils.cs
@@ -3,9 +3,9 @@
using EventLogExpert.Eventing.Providers;
using EventLogExpert.Eventing.Readers;
-using static EventLogExpert.Eventing.Tests.TestUtils.Constants.Constants;
+using static EventLogExpert.Eventing.TestUtils.Constants.Constants;
-namespace EventLogExpert.Eventing.Tests.TestUtils;
+namespace EventLogExpert.Eventing.TestUtils;
public static class EventUtils
{
diff --git a/tests/Shared/EventLogExpert.Eventing.TestUtils/SqliteTestDb.cs b/tests/Shared/EventLogExpert.Eventing.TestUtils/SqliteTestDb.cs
new file mode 100644
index 00000000..a0696aa9
--- /dev/null
+++ b/tests/Shared/EventLogExpert.Eventing.TestUtils/SqliteTestDb.cs
@@ -0,0 +1,96 @@
+// // Copyright (c) Microsoft Corporation.
+// // Licensed under the MIT License.
+
+using Microsoft.Data.Sqlite;
+
+namespace EventLogExpert.Eventing.TestUtils;
+
+/// Helpers for managing SQLite test database files.
+public static class SqliteTestDb
+{
+ /// Deletes a SQLite test database file with retries, after first releasing pooled SqliteConnection handles.
+ ///
+ ///
+ /// uses EF Core's default pooled
+ /// SqliteConnection. Without the pooled handle still owns the file
+ /// after the context is disposed and hits a sharing violation.
+ ///
+ ///
+ /// mutates process-wide state, so it must only be called from a
+ /// test assembly configured for serial execution (xunit.runner.json with parallelizeTestCollections: false)
+ /// — otherwise it can race with concurrent SQLite work on another thread.
+ ///
+ ///
+ public static void Delete(string? path, int maxAttempts = 10, int delayMs = 200)
+ {
+ if (string.IsNullOrEmpty(path) || !File.Exists(path)) { return; }
+
+ SqliteConnection.ClearAllPools();
+
+ for (int i = 0; i < maxAttempts; i++)
+ {
+ try
+ {
+ File.Delete(path);
+ return;
+ }
+ catch (IOException) when (i < maxAttempts - 1)
+ {
+ Thread.Sleep(delayMs);
+ }
+ catch (UnauthorizedAccessException) when (i < maxAttempts - 1)
+ {
+ Thread.Sleep(delayMs);
+ }
+ catch (IOException)
+ {
+ // Best-effort cleanup; swallow last IOException — OS will reclaim temp files.
+ return;
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return;
+ }
+ }
+ }
+
+ ///
+ /// Recursively deletes a directory containing SQLite test database files, with retries, after first releasing
+ /// pooled SqliteConnection handles.
+ ///
+ ///
+ /// Same caveats as : mutates process-wide
+ /// state, so this helper must only be called from a test assembly configured for serial execution.
+ ///
+ public static void DeleteDirectory(string? path, int maxAttempts = 10, int delayMs = 200)
+ {
+ if (string.IsNullOrEmpty(path) || !Directory.Exists(path)) { return; }
+
+ SqliteConnection.ClearAllPools();
+
+ for (int i = 0; i < maxAttempts; i++)
+ {
+ try
+ {
+ Directory.Delete(path, recursive: true);
+ return;
+ }
+ catch (IOException) when (i < maxAttempts - 1)
+ {
+ Thread.Sleep(delayMs);
+ }
+ catch (UnauthorizedAccessException) when (i < maxAttempts - 1)
+ {
+ Thread.Sleep(delayMs);
+ }
+ catch (IOException)
+ {
+ return;
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return;
+ }
+ }
+ }
+}
diff --git a/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs b/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs
index b5826b23..fcd087db 100644
--- a/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs
+++ b/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs
@@ -66,29 +66,29 @@ public void BannerHost_AttentionDismissed_DoesNotRenderAttentionBanner()
}
[Fact]
- public void BannerHost_AttentionEntriesSingleEntry_UsesSingularDatabaseLabel()
+ public void BannerHost_AttentionEntries_RendersAttentionBannerWithOpenSettingsAndDismiss()
{
- _bannerService.AttentionEntries.Returns([BuildDatabaseEntry("a.db")]);
+ _bannerService.AttentionEntries.Returns(
+ [BuildDatabaseEntry("a.db"), BuildDatabaseEntry("b.db")]);
var component = Render();
var banner = component.Find("aside.banner-attention");
- Assert.Contains("1 database need", banner.TextContent);
- Assert.DoesNotContain("databases need", banner.TextContent);
+ Assert.Contains("2 databases need attention", banner.TextContent);
+ Assert.Equal("Open Settings", component.Find("aside.banner-attention button.banner-action").TextContent.Trim());
+ Assert.Single(component.FindAll("aside.banner-attention button.banner-dismiss"));
}
[Fact]
- public void BannerHost_AttentionEntries_RendersAttentionBannerWithOpenSettingsAndDismiss()
+ public void BannerHost_AttentionEntriesSingleEntry_UsesSingularDatabaseLabel()
{
- _bannerService.AttentionEntries.Returns(
- [BuildDatabaseEntry("a.db"), BuildDatabaseEntry("b.db")]);
+ _bannerService.AttentionEntries.Returns([BuildDatabaseEntry("a.db")]);
var component = Render();
var banner = component.Find("aside.banner-attention");
- Assert.Contains("2 databases need attention", banner.TextContent);
- Assert.Equal("Open Settings", component.Find("aside.banner-attention button.banner-action").TextContent.Trim());
- Assert.Single(component.FindAll("aside.banner-attention button.banner-dismiss"));
+ Assert.Contains("1 database need", banner.TextContent);
+ Assert.DoesNotContain("databases need", banner.TextContent);
}
[Fact]
diff --git a/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseEntryRowTests.cs b/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseEntryRowTests.cs
index a921a645..b8cb97ee 100644
--- a/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseEntryRowTests.cs
+++ b/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseEntryRowTests.cs
@@ -238,34 +238,34 @@ public void Render_ReadyEnabledEntry_RendersTrashButton()
}
[Fact]
- public void Render_ReadyEntryWithClassificationPending_ShowsDisabledToggle()
+ public void Render_ReadyEntry_ShowsToggle_AndNoBadge()
{
// Arrange
var entry = MakeEntry(DatabaseStatus.Ready);
// Act
- var component = RenderRow(entry, isClassificationPending: true);
+ var component = RenderRow(entry);
// Assert
- var radios = component.FindAll(".toggle input[type='radio']");
- Assert.NotEmpty(radios);
- Assert.All(radios, r => Assert.True(r.HasAttribute("disabled")));
+ Assert.Single(component.FindAll(".toggle"));
+ Assert.Empty(component.FindAll(".db-entry-badge"));
+ Assert.Empty(component.FindAll(".db-entry-upgrade-btn"));
+ Assert.Empty(component.FindAll(".db-entry-upgrading"));
}
[Fact]
- public void Render_ReadyEntry_ShowsToggle_AndNoBadge()
+ public void Render_ReadyEntryWithClassificationPending_ShowsDisabledToggle()
{
// Arrange
var entry = MakeEntry(DatabaseStatus.Ready);
// Act
- var component = RenderRow(entry);
+ var component = RenderRow(entry, isClassificationPending: true);
// Assert
- Assert.Single(component.FindAll(".toggle"));
- Assert.Empty(component.FindAll(".db-entry-badge"));
- Assert.Empty(component.FindAll(".db-entry-upgrade-btn"));
- Assert.Empty(component.FindAll(".db-entry-upgrading"));
+ var radios = component.FindAll(".toggle input[type='radio']");
+ Assert.NotEmpty(radios);
+ Assert.All(radios, r => Assert.True(r.HasAttribute("disabled")));
}
[Fact]
diff --git a/tests/Unit/EventLogExpert.Components.Tests/Database/SettingsUpgradeProgressBannerTests.cs b/tests/Unit/EventLogExpert.Components.Tests/Database/SettingsUpgradeProgressBannerTests.cs
index 42fc3388..c28f1130 100644
--- a/tests/Unit/EventLogExpert.Components.Tests/Database/SettingsUpgradeProgressBannerTests.cs
+++ b/tests/Unit/EventLogExpert.Components.Tests/Database/SettingsUpgradeProgressBannerTests.cs
@@ -81,11 +81,11 @@ public void SettingsUpgradeProgressBanner_DisposeIsIdempotent()
// Arrange
var component = Render();
var instance = component.Instance;
-
- // Act + Assert
- instance.Dispose();
instance.Dispose();
+ // Act + Assert - second Dispose must not throw
+ var exception = Record.Exception(() => instance.Dispose());
+ Assert.Null(exception);
}
[Fact]
diff --git a/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs b/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs
index 041e2f0b..909d4082 100644
--- a/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs
+++ b/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs
@@ -217,13 +217,12 @@ await component.WaitForAssertionAsync(() =>
}
[Fact]
- public async Task DebugLogModal_CopyClickWhilePendingFilterPending_FlushesFilterBeforeReadingDisplayed()
+ public async Task DebugLogModal_CopyClick_CallsCopyTextAsyncWithEnvironmentNewLineJoinedDisplayed()
{
// Arrange
var lines = new[]
{
DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogFirstMessage),
- DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogErrorMessage),
DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogSecondMessage),
};
@@ -232,25 +231,25 @@ public async Task DebugLogModal_CopyClickWhilePendingFilterPending_FlushesFilter
var component = Render();
await component.WaitForAssertionAsync(() =>
- Assert.Equal("3 of 3 entries", component.Find(".debug-log-footer-counter").TextContent.Trim()));
+ Assert.Equal("2 of 2 entries", component.Find(".debug-log-footer-counter").TextContent.Trim()));
- // Act — typed text doesn't apply until the 250ms debounce; Copy must flush first.
- await component.Find("input[aria-label='Filter messages']").InputAsync(new ChangeEventArgs { Value = "error" });
+ var expectedContent = string.Join(Environment.NewLine, new[] { lines[1], lines[0] });
+
+ // Act
await component.Find("button:contains('Copy')").ClickAsync(new MouseEventArgs());
- // Assert: clipboard receives only the entry whose Message contains "error".
- await _clipboardService.Received(1).CopyTextAsync(lines[1]);
- await _clipboardService.DidNotReceive().CopyTextAsync(
- Arg.Is(s => s.Contains(Constants.DebugLogFirstMessage)));
+ // Assert
+ await _clipboardService.Received(1).CopyTextAsync(expectedContent);
}
[Fact]
- public async Task DebugLogModal_CopyClick_CallsCopyTextAsyncWithEnvironmentNewLineJoinedDisplayed()
+ public async Task DebugLogModal_CopyClickWhilePendingFilterPending_FlushesFilterBeforeReadingDisplayed()
{
// Arrange
var lines = new[]
{
DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogFirstMessage),
+ DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogErrorMessage),
DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogSecondMessage),
};
@@ -259,15 +258,16 @@ public async Task DebugLogModal_CopyClick_CallsCopyTextAsyncWithEnvironmentNewLi
var component = Render();
await component.WaitForAssertionAsync(() =>
- Assert.Equal("2 of 2 entries", component.Find(".debug-log-footer-counter").TextContent.Trim()));
-
- var expectedContent = string.Join(Environment.NewLine, new[] { lines[1], lines[0] });
+ Assert.Equal("3 of 3 entries", component.Find(".debug-log-footer-counter").TextContent.Trim()));
- // Act
+ // Act — typed text doesn't apply until the 250ms debounce; Copy must flush first.
+ await component.Find("input[aria-label='Filter messages']").InputAsync(new ChangeEventArgs { Value = "error" });
await component.Find("button:contains('Copy')").ClickAsync(new MouseEventArgs());
- // Assert
- await _clipboardService.Received(1).CopyTextAsync(expectedContent);
+ // Assert: clipboard receives only the entry whose Message contains "error".
+ await _clipboardService.Received(1).CopyTextAsync(lines[1]);
+ await _clipboardService.DidNotReceive().CopyTextAsync(
+ Arg.Is(s => s.Contains(Constants.DebugLogFirstMessage)));
}
[Fact]
@@ -309,7 +309,7 @@ await component.WaitForAssertionAsync(
}
[Fact]
- public async Task DebugLogModal_EmptyLogWithActiveFilter_StillShowsLogIsEmptyMessage()
+ public async Task DebugLogModal_EmptyLog_CopyButtonIsDisabled()
{
// Arrange
_fileLogger.LoadAsync(Arg.Any()).Returns(DebugLogUtils.ToAsyncEnumerable([]));
@@ -319,19 +319,14 @@ public async Task DebugLogModal_EmptyLogWithActiveFilter_StillShowsLogIsEmptyMes
await component.WaitForAssertionAsync(() =>
Assert.Equal("0 of 0 entries", component.Find(".debug-log-footer-counter").TextContent.Trim()));
- // Act: type a filter while the log is empty
- await component.Find("input[aria-label='Filter messages']").InputAsync(new ChangeEventArgs { Value = "any-search-text" });
+ // Act + Assert
+ var copyButton = component.Find("button:contains('Copy')");
- // Assert — zero entries (not zero filtered); "No entries match filters" would mislead.
- await component.WaitForAssertionAsync(() =>
- {
- Assert.Contains("Log is Empty...", component.Markup);
- Assert.DoesNotContain("No entries match filters.", component.Markup);
- });
+ Assert.True(copyButton.HasAttribute("disabled"));
}
[Fact]
- public async Task DebugLogModal_EmptyLog_CopyButtonIsDisabled()
+ public async Task DebugLogModal_EmptyLog_ExportButtonIsDisabled()
{
// Arrange
_fileLogger.LoadAsync(Arg.Any()).Returns(DebugLogUtils.ToAsyncEnumerable([]));
@@ -342,52 +337,56 @@ await component.WaitForAssertionAsync(() =>
Assert.Equal("0 of 0 entries", component.Find(".debug-log-footer-counter").TextContent.Trim()));
// Act + Assert
- var copyButton = component.Find("button:contains('Copy')");
+ var exportButton = component.Find("button:contains('Export')");
- Assert.True(copyButton.HasAttribute("disabled"));
+ Assert.True(exportButton.HasAttribute("disabled"));
}
[Fact]
- public async Task DebugLogModal_EmptyLog_ExportButtonIsDisabled()
+ public async Task DebugLogModal_EmptyLog_ShowsLogIsEmptyMessageAndZeroCounter()
{
// Arrange
_fileLogger.LoadAsync(Arg.Any()).Returns(DebugLogUtils.ToAsyncEnumerable([]));
+ // Act
var component = Render();
+ // Assert
await component.WaitForAssertionAsync(() =>
Assert.Equal("0 of 0 entries", component.Find(".debug-log-footer-counter").TextContent.Trim()));
- // Act + Assert
- var exportButton = component.Find("button:contains('Export')");
-
- Assert.True(exportButton.HasAttribute("disabled"));
+ Assert.Contains("Log is Empty...", component.Markup);
}
[Fact]
- public async Task DebugLogModal_EmptyLog_ShowsLogIsEmptyMessageAndZeroCounter()
+ public async Task DebugLogModal_EmptyLogWithActiveFilter_StillShowsLogIsEmptyMessage()
{
// Arrange
_fileLogger.LoadAsync(Arg.Any()).Returns(DebugLogUtils.ToAsyncEnumerable([]));
- // Act
var component = Render();
- // Assert
await component.WaitForAssertionAsync(() =>
Assert.Equal("0 of 0 entries", component.Find(".debug-log-footer-counter").TextContent.Trim()));
- Assert.Contains("Log is Empty...", component.Markup);
+ // Act: type a filter while the log is empty
+ await component.Find("input[aria-label='Filter messages']").InputAsync(new ChangeEventArgs { Value = "any-search-text" });
+
+ // Assert — zero entries (not zero filtered); "No entries match filters" would mislead.
+ await component.WaitForAssertionAsync(() =>
+ {
+ Assert.Contains("Log is Empty...", component.Markup);
+ Assert.DoesNotContain("No entries match filters.", component.Markup);
+ });
}
[Fact]
- public async Task DebugLogModal_ExportClickWhilePendingFilterPending_FlushesFilterBeforeReadingDisplayed()
+ public async Task DebugLogModal_ExportClick_CallsSaveAsyncWithEnvironmentNewLineJoinedDisplayed()
{
// Arrange
var lines = new[]
{
DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogFirstMessage),
- DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogErrorMessage),
DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogSecondMessage),
};
@@ -404,29 +403,31 @@ public async Task DebugLogModal_ExportClickWhilePendingFilterPending_FlushesFilt
var component = Render();
await component.WaitForAssertionAsync(() =>
- Assert.Equal("3 of 3 entries", component.Find(".debug-log-footer-counter").TextContent.Trim()));
+ Assert.Equal("2 of 2 entries", component.Find(".debug-log-footer-counter").TextContent.Trim()));
- // Act: type a filter that drops two entries, then click Export before debounce expires.
- await component.Find("input[aria-label='Filter messages']").InputAsync(new ChangeEventArgs { Value = "error" });
+ var expectedContent = string.Join(Environment.NewLine, new[] { lines[1], lines[0] });
+
+ // Act
await component.Find("button:contains('Export')").ClickAsync(new MouseEventArgs());
// Assert
await _fileSaveService.Received(1).SaveAsync(
- Arg.Any(),
+ Arg.Is(name => name.StartsWith("debug-log-") && name.EndsWith(".log")),
FileSaveFileTypes.Log,
Arg.Any>());
Assert.NotNull(capturedWriter);
- Assert.Equal(lines[1], await InvokeWriterAndDecodeAsync(capturedWriter));
+ Assert.Equal(expectedContent, await InvokeWriterAndDecodeAsync(capturedWriter));
}
[Fact]
- public async Task DebugLogModal_ExportClick_CallsSaveAsyncWithEnvironmentNewLineJoinedDisplayed()
+ public async Task DebugLogModal_ExportClickWhilePendingFilterPending_FlushesFilterBeforeReadingDisplayed()
{
// Arrange
var lines = new[]
{
DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogFirstMessage),
+ DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogErrorMessage),
DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogSecondMessage),
};
@@ -443,21 +444,20 @@ public async Task DebugLogModal_ExportClick_CallsSaveAsyncWithEnvironmentNewLine
var component = Render();
await component.WaitForAssertionAsync(() =>
- Assert.Equal("2 of 2 entries", component.Find(".debug-log-footer-counter").TextContent.Trim()));
-
- var expectedContent = string.Join(Environment.NewLine, new[] { lines[1], lines[0] });
+ Assert.Equal("3 of 3 entries", component.Find(".debug-log-footer-counter").TextContent.Trim()));
- // Act
+ // Act: type a filter that drops two entries, then click Export before debounce expires.
+ await component.Find("input[aria-label='Filter messages']").InputAsync(new ChangeEventArgs { Value = "error" });
await component.Find("button:contains('Export')").ClickAsync(new MouseEventArgs());
// Assert
await _fileSaveService.Received(1).SaveAsync(
- Arg.Is(name => name.StartsWith("debug-log-") && name.EndsWith(".log")),
+ Arg.Any(),
FileSaveFileTypes.Log,
Arg.Any>());
Assert.NotNull(capturedWriter);
- Assert.Equal(expectedContent, await InvokeWriterAndDecodeAsync(capturedWriter));
+ Assert.Equal(lines[1], await InvokeWriterAndDecodeAsync(capturedWriter));
}
[Fact]
@@ -589,7 +589,8 @@ await component.WaitForAssertionAsync(
TimeSpan.FromSeconds(2));
// Act
- var levelDropdown = component.Find("input[aria-label='Level']").ParentElement!;
+ var levelDropdown = component.Find("input[aria-label='Level']").ParentElement;
+ Assert.NotNull(levelDropdown);
var errorOption = levelDropdown.QuerySelectorAll("[role='option']")
.First(o => o.TextContent.Trim() == "Error");
@@ -630,7 +631,8 @@ await component.WaitForAssertionAsync(() =>
// Act — type a string filter (debounce starts), then change the level dropdown before it elapses.
await component.Find("input[aria-label='Filter messages']").InputAsync(new ChangeEventArgs { Value = "matching" });
- var levelDropdown = component.Find("input[aria-label='Level']").ParentElement!;
+ var levelDropdown = component.Find("input[aria-label='Level']").ParentElement;
+ Assert.NotNull(levelDropdown);
var errorOption = levelDropdown.QuerySelectorAll("[role='option']")
.First(o => o.TextContent.Trim() == "Error");
@@ -660,7 +662,8 @@ await component.WaitForAssertionAsync(() =>
Assert.Equal("3 of 3 entries", component.Find(".debug-log-footer-counter").TextContent.Trim()));
// Act: click "Error" in the level value dropdown
- var levelDropdown = component.Find("input[aria-label='Level']").ParentElement!;
+ var levelDropdown = component.Find("input[aria-label='Level']").ParentElement;
+ Assert.NotNull(levelDropdown);
var errorOption = levelDropdown.QuerySelectorAll("[role='option']")
.First(o => o.TextContent.Trim() == "Error");
@@ -690,14 +693,16 @@ await component.WaitForAssertionAsync(() =>
Assert.Equal("3 of 3 entries", component.Find(".debug-log-footer-counter").TextContent.Trim()));
// Act: switch operator to "Multi Select"
- var operatorDropdown = component.Find("input[aria-label='Level operator']").ParentElement!;
+ var operatorDropdown = component.Find("input[aria-label='Level operator']").ParentElement;
+ Assert.NotNull(operatorDropdown);
var multiSelectOption = operatorDropdown.QuerySelectorAll("[role='option']")
.First(o => o.TextContent.Trim() == "Multi Select");
await multiSelectOption.MouseDownAsync(new MouseEventArgs());
// Then pick Error and Warning from the multi-select dropdown
- var levelsDropdown = component.Find("input[aria-label='Levels']").ParentElement!;
+ var levelsDropdown = component.Find("input[aria-label='Levels']").ParentElement;
+ Assert.NotNull(levelsDropdown);
var errorOption = levelsDropdown.QuerySelectorAll("[role='option']")
.First(o => o.TextContent.Trim() == "Error");
@@ -733,14 +738,16 @@ await component.WaitForAssertionAsync(() =>
Assert.Equal("3 of 3 entries", component.Find(".debug-log-footer-counter").TextContent.Trim()));
// Act: switch operator to "Not Equal"
- var operatorDropdown = component.Find("input[aria-label='Level operator']").ParentElement!;
+ var operatorDropdown = component.Find("input[aria-label='Level operator']").ParentElement;
+ Assert.NotNull(operatorDropdown);
var notEqualOption = operatorDropdown.QuerySelectorAll("[role='option']")
.First(o => o.TextContent.Trim() == "Not Equal");
await notEqualOption.MouseDownAsync(new MouseEventArgs());
// Then select "Information" as the value to exclude
- var levelDropdown = component.Find("input[aria-label='Level']").ParentElement!;
+ var levelDropdown = component.Find("input[aria-label='Level']").ParentElement;
+ Assert.NotNull(levelDropdown);
var informationOption = levelDropdown.QuerySelectorAll("[role='option']")
.First(o => o.TextContent.Trim() == "Information");
diff --git a/tests/Unit/EventLogExpert.EventDbTool.Tests/ProgramTests.cs b/tests/Unit/EventLogExpert.EventDbTool.Tests/ProgramTests.cs
new file mode 100644
index 00000000..90ba9271
--- /dev/null
+++ b/tests/Unit/EventLogExpert.EventDbTool.Tests/ProgramTests.cs
@@ -0,0 +1,49 @@
+// // Copyright (c) Microsoft Corporation.
+// // Licensed under the MIT License.
+
+using EventLogExpert.Eventing.Logging;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace EventLogExpert.EventDbTool.Tests;
+
+public sealed class ProgramTests
+{
+ [Fact]
+ public void BuildServiceProvider_RegistersITraceLoggerAsSingleton()
+ {
+ // Arrange / Act — the same provider should hand out the same logger instance for repeated
+ // resolves, which is the behavior the rest of the tool depends on (a per-call logger would
+ // double-buffer trace output and lose verbose-level config).
+ using var sp = Program.BuildServiceProvider(verbose: false);
+
+ var first = sp.GetRequiredService();
+ var second = sp.GetRequiredService();
+
+ // Assert
+ Assert.Same(first, second);
+ }
+
+ [Fact]
+ public void BuildServiceProvider_WhenVerboseFalse_RegistersLoggerAtInformationLevel()
+ {
+ // Default CLI verbosity is Information so users see progress but not internal trace noise.
+ using var sp = Program.BuildServiceProvider(verbose: false);
+
+ var logger = sp.GetRequiredService();
+
+ Assert.Equal(LogLevel.Information, logger.MinimumLevel);
+ }
+
+ [Fact]
+ public void BuildServiceProvider_WhenVerboseTrue_RegistersLoggerAtTraceLevel()
+ {
+ // The --verbose flag is the only way to surface Trace/Debug logs, so the contract that
+ // verbose=true ⇒ MinimumLevel=Trace is load-bearing for troubleshooting workflows.
+ using var sp = Program.BuildServiceProvider(verbose: true);
+
+ var logger = sp.GetRequiredService();
+
+ Assert.Equal(LogLevel.Trace, logger.MinimumLevel);
+ }
+}
diff --git a/tests/Unit/EventLogExpert.EventDbTool.Tests/RegexHelperTests.cs b/tests/Unit/EventLogExpert.EventDbTool.Tests/RegexHelperTests.cs
new file mode 100644
index 00000000..37898391
--- /dev/null
+++ b/tests/Unit/EventLogExpert.EventDbTool.Tests/RegexHelperTests.cs
@@ -0,0 +1,89 @@
+// // Copyright (c) Microsoft Corporation.
+// // Licensed under the MIT License.
+
+using EventLogExpert.Eventing.Logging;
+using NSubstitute;
+
+namespace EventLogExpert.EventDbTool.Tests;
+
+public sealed class RegexHelperTests
+{
+ [Fact]
+ public void TryCreate_WhenPatternIsEmpty_ReturnsTrueAndNoRegex()
+ {
+ // Arrange
+ var logger = Substitute.For();
+
+ // Act
+ var success = RegexHelper.TryCreate(string.Empty, logger, out var regex);
+
+ // Assert — empty pattern means "no filter", caller must distinguish from a malformed one.
+ Assert.True(success);
+ Assert.Null(regex);
+ logger.DidNotReceive().Error(Arg.Any());
+ }
+
+ [Fact]
+ public void TryCreate_WhenPatternIsInvalid_ReturnsFalseLogsErrorAndYieldsNullRegex()
+ {
+ // Arrange — unbalanced character class is a parse-time ArgumentException.
+ var logger = Substitute.For();
+
+ // Act
+ var success = RegexHelper.TryCreate("[unclosed", logger, out var regex);
+
+ // Assert
+ Assert.False(success);
+ Assert.Null(regex);
+ logger.Received(1).Error(Arg.Is(h =>
+ h.ToString().Contains("Invalid --filter regex") && h.ToString().Contains("[unclosed")));
+ }
+
+ [Fact]
+ public void TryCreate_WhenPatternIsNull_ReturnsTrueAndNoRegex()
+ {
+ // Arrange
+ var logger = Substitute.For();
+
+ // Act
+ var success = RegexHelper.TryCreate(null, logger, out var regex);
+
+ // Assert
+ Assert.True(success);
+ Assert.Null(regex);
+ logger.DidNotReceive().Error(Arg.Any());
+ }
+
+ [Fact]
+ public void TryCreate_WhenPatternIsValid_HasOneSecondMatchTimeout()
+ {
+ // Arrange
+ var logger = Substitute.For();
+
+ // Act
+ RegexHelper.TryCreate("foo", logger, out var regex);
+
+ // Assert — the timeout bound prevents catastrophic-backtracking patterns from hanging the
+ // process. Pinning the exact 1-second value ensures a regression that quietly bumped this
+ // to InfiniteMatchTimeout (or to many minutes) would fail this test.
+ Assert.NotNull(regex);
+ Assert.Equal(TimeSpan.FromSeconds(1), regex.MatchTimeout);
+ }
+
+ [Fact]
+ public void TryCreate_WhenPatternIsValid_ReturnsTrueAndCaseInsensitiveRegex()
+ {
+ // Arrange
+ var logger = Substitute.For();
+
+ // Act
+ var success = RegexHelper.TryCreate("microsoft-windows-.*", logger, out var regex);
+
+ // Assert — case insensitivity is contractual: provider names like "Microsoft-Windows-AAD" must match.
+ Assert.True(success);
+ Assert.NotNull(regex);
+ Assert.Matches(regex, "Microsoft-Windows-AAD");
+ Assert.Matches(regex, "MICROSOFT-WINDOWS-FOO");
+ Assert.DoesNotMatch(regex, "OpenSSH");
+ }
+}
diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Common/Channels/LogChannelMethodsTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Common/Channels/LogChannelMethodsTests.cs
index 94188fda..011a656b 100644
--- a/tests/Unit/EventLogExpert.Eventing.Tests/Common/Channels/LogChannelMethodsTests.cs
+++ b/tests/Unit/EventLogExpert.Eventing.Tests/Common/Channels/LogChannelMethodsTests.cs
@@ -39,19 +39,19 @@ public void GetMenuPath_WhenLogNameContainsSpaces_ShouldKeepSegmentIntact()
}
[Fact]
- public void GetMenuPath_WhenMicrosoftWindowsChannelHasHyphen_ShouldKeepChannelIntact()
+ public void GetMenuPath_WhenMicrosoftWindowsChannel_ShouldExpandToFourSegments()
{
- var path = LogChannelMethods.GetMenuPath("Microsoft-Windows-Kernel-Power/Thermal-Operational");
+ var path = LogChannelMethods.GetMenuPath("Microsoft-Windows-AAD/Operational");
- Assert.Equal(["Microsoft", "Windows", "Kernel-Power", "Thermal-Operational"], path);
+ Assert.Equal(["Microsoft", "Windows", "AAD", "Operational"], path);
}
[Fact]
- public void GetMenuPath_WhenMicrosoftWindowsChannel_ShouldExpandToFourSegments()
+ public void GetMenuPath_WhenMicrosoftWindowsChannelHasHyphen_ShouldKeepChannelIntact()
{
- var path = LogChannelMethods.GetMenuPath("Microsoft-Windows-AAD/Operational");
+ var path = LogChannelMethods.GetMenuPath("Microsoft-Windows-Kernel-Power/Thermal-Operational");
- Assert.Equal(["Microsoft", "Windows", "AAD", "Operational"], path);
+ Assert.Equal(["Microsoft", "Windows", "Kernel-Power", "Thermal-Operational"], path);
}
[Fact]
diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/EventLogExpert.Eventing.Tests.csproj b/tests/Unit/EventLogExpert.Eventing.Tests/EventLogExpert.Eventing.Tests.csproj
index e717f1db..385033d9 100644
--- a/tests/Unit/EventLogExpert.Eventing.Tests/EventLogExpert.Eventing.Tests.csproj
+++ b/tests/Unit/EventLogExpert.Eventing.Tests/EventLogExpert.Eventing.Tests.csproj
@@ -8,6 +8,7 @@
+
diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Interop/NativeMethodsEvtTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Interop/NativeMethodsEvtTests.cs
index 291df175..062ebd08 100644
--- a/tests/Unit/EventLogExpert.Eventing.Tests/Interop/NativeMethodsEvtTests.cs
+++ b/tests/Unit/EventLogExpert.Eventing.Tests/Interop/NativeMethodsEvtTests.cs
@@ -92,18 +92,19 @@ public void ConvertVariant_WhenBooleanTrue_ShouldReturnTrue()
}
[Fact]
- public void ConvertVariant_WhenByteArrayEmpty_ShouldReturnEmptyArray()
+ public void ConvertVariant_WhenByte_ShouldReturnByte()
{
// Arrange
- var variant = CreateVariantWithCount(EvtVariantType.ByteArray, IntPtr.Zero, 0);
+ byte expectedValue = 255;
+ var variant = CreateVariant(EvtVariantType.Byte, expectedValue);
// Act
var result = NativeMethods.ConvertVariant(variant);
// Assert
Assert.NotNull(result);
- Assert.IsType(result);
- Assert.Empty((byte[])result);
+ Assert.IsType(result);
+ Assert.Equal(expectedValue, result);
}
[Fact]
@@ -134,19 +135,18 @@ public void ConvertVariant_WhenByteArray_ShouldReturnByteArray()
}
[Fact]
- public void ConvertVariant_WhenByte_ShouldReturnByte()
+ public void ConvertVariant_WhenByteArrayEmpty_ShouldReturnEmptyArray()
{
// Arrange
- byte expectedValue = 255;
- var variant = CreateVariant(EvtVariantType.Byte, expectedValue);
+ var variant = CreateVariantWithCount(EvtVariantType.ByteArray, IntPtr.Zero, 0);
// Act
var result = NativeMethods.ConvertVariant(variant);
// Assert
Assert.NotNull(result);
- Assert.IsType(result);
- Assert.Equal(expectedValue, result);
+ Assert.IsType(result);
+ Assert.Empty((byte[])result);
}
[Theory]
@@ -231,18 +231,19 @@ public void ConvertVariant_WhenGuid_ShouldReturnGuid()
}
[Fact]
- public void ConvertVariant_WhenHexInt32ArrayEmpty_ShouldReturnEmptyArray()
+ public void ConvertVariant_WhenHexInt32_ShouldReturnInt32()
{
// Arrange
- var variant = CreateVariantWithCount(EvtVariantType.HexInt32Array, IntPtr.Zero, 0);
+ int expectedValue = unchecked((int)0x1234ABCD);
+ var variant = CreateVariant(EvtVariantType.HexInt32, expectedValue);
// Act
var result = NativeMethods.ConvertVariant(variant);
// Assert
Assert.NotNull(result);
- Assert.IsType(result);
- Assert.Empty((int[])result);
+ Assert.IsType(result);
+ Assert.Equal(expectedValue, result);
}
[Fact]
@@ -277,19 +278,18 @@ public void ConvertVariant_WhenHexInt32Array_ShouldReturnInt32Array()
}
[Fact]
- public void ConvertVariant_WhenHexInt32_ShouldReturnInt32()
+ public void ConvertVariant_WhenHexInt32ArrayEmpty_ShouldReturnEmptyArray()
{
// Arrange
- int expectedValue = unchecked((int)0x1234ABCD);
- var variant = CreateVariant(EvtVariantType.HexInt32, expectedValue);
+ var variant = CreateVariantWithCount(EvtVariantType.HexInt32Array, IntPtr.Zero, 0);
// Act
var result = NativeMethods.ConvertVariant(variant);
// Assert
Assert.NotNull(result);
- Assert.IsType(result);
- Assert.Equal(expectedValue, result);
+ Assert.IsType(result);
+ Assert.Empty((int[])result);
}
[Fact]
@@ -463,18 +463,28 @@ public void ConvertVariant_WhenSizeT_ShouldReturnIntPtr()
}
[Fact]
- public void ConvertVariant_WhenStringArrayEmpty_ShouldReturnEmptyArray()
+ public void ConvertVariant_WhenString_ShouldReturnString()
{
// Arrange
- var variant = CreateVariantWithCount(EvtVariantType.StringArray, IntPtr.Zero, 0);
+ var testString = "Test String";
+ IntPtr stringPtr = Marshal.StringToHGlobalUni(testString);
- // Act
- var result = NativeMethods.ConvertVariant(variant);
+ try
+ {
+ var variant = CreateVariant(EvtVariantType.String, stringPtr);
- // Assert
- Assert.NotNull(result);
- Assert.IsType(result);
- Assert.Empty((string[])result);
+ // Act
+ var result = NativeMethods.ConvertVariant(variant);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.IsType(result);
+ Assert.Equal(testString, result);
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(stringPtr);
+ }
}
[Fact]
@@ -518,28 +528,18 @@ public void ConvertVariant_WhenStringArray_ShouldReturnStringArray()
}
[Fact]
- public void ConvertVariant_WhenString_ShouldReturnString()
+ public void ConvertVariant_WhenStringArrayEmpty_ShouldReturnEmptyArray()
{
// Arrange
- var testString = "Test String";
- IntPtr stringPtr = Marshal.StringToHGlobalUni(testString);
-
- try
- {
- var variant = CreateVariant(EvtVariantType.String, stringPtr);
+ var variant = CreateVariantWithCount(EvtVariantType.StringArray, IntPtr.Zero, 0);
- // Act
- var result = NativeMethods.ConvertVariant(variant);
+ // Act
+ var result = NativeMethods.ConvertVariant(variant);
- // Assert
- Assert.NotNull(result);
- Assert.IsType(result);
- Assert.Equal(testString, result);
- }
- finally
- {
- Marshal.FreeHGlobal(stringPtr);
- }
+ // Assert
+ Assert.NotNull(result);
+ Assert.IsType(result);
+ Assert.Empty((string[])result);
}
[Fact]
@@ -581,18 +581,19 @@ public void ConvertVariant_WhenSysTime_ShouldReturnDateTime()
}
[Fact]
- public void ConvertVariant_WhenUInt16ArrayEmpty_ShouldReturnEmptyArray()
+ public void ConvertVariant_WhenUInt16_ShouldReturnUInt16()
{
// Arrange
- var variant = CreateVariantWithCount(EvtVariantType.UInt16Array, IntPtr.Zero, 0);
+ ushort expectedValue = 65000;
+ var variant = CreateVariant(EvtVariantType.UInt16, expectedValue);
// Act
var result = NativeMethods.ConvertVariant(variant);
// Assert
Assert.NotNull(result);
- Assert.IsType(result);
- Assert.Empty((ushort[])result);
+ Assert.IsType(result);
+ Assert.Equal(expectedValue, result);
}
[Fact]
@@ -627,34 +628,34 @@ public void ConvertVariant_WhenUInt16Array_ShouldReturnUInt16Array()
}
[Fact]
- public void ConvertVariant_WhenUInt16_ShouldReturnUInt16()
+ public void ConvertVariant_WhenUInt16ArrayEmpty_ShouldReturnEmptyArray()
{
// Arrange
- ushort expectedValue = 65000;
- var variant = CreateVariant(EvtVariantType.UInt16, expectedValue);
+ var variant = CreateVariantWithCount(EvtVariantType.UInt16Array, IntPtr.Zero, 0);
// Act
var result = NativeMethods.ConvertVariant(variant);
// Assert
Assert.NotNull(result);
- Assert.IsType(result);
- Assert.Equal(expectedValue, result);
+ Assert.IsType(result);
+ Assert.Empty((ushort[])result);
}
[Fact]
- public void ConvertVariant_WhenUInt32ArrayEmpty_ShouldReturnEmptyArray()
+ public void ConvertVariant_WhenUInt32_ShouldReturnUInt32()
{
// Arrange
- var variant = CreateVariantWithCount(EvtVariantType.UInt32Array, IntPtr.Zero, 0);
+ uint expectedValue = 4000000000;
+ var variant = CreateVariant(EvtVariantType.UInt32, expectedValue);
// Act
var result = NativeMethods.ConvertVariant(variant);
// Assert
Assert.NotNull(result);
- Assert.IsType(result);
- Assert.Empty((uint[])result);
+ Assert.IsType(result);
+ Assert.Equal(expectedValue, result);
}
[Fact]
@@ -689,19 +690,18 @@ public void ConvertVariant_WhenUInt32Array_ShouldReturnUInt32Array()
}
[Fact]
- public void ConvertVariant_WhenUInt32_ShouldReturnUInt32()
+ public void ConvertVariant_WhenUInt32ArrayEmpty_ShouldReturnEmptyArray()
{
// Arrange
- uint expectedValue = 4000000000;
- var variant = CreateVariant(EvtVariantType.UInt32, expectedValue);
+ var variant = CreateVariantWithCount(EvtVariantType.UInt32Array, IntPtr.Zero, 0);
// Act
var result = NativeMethods.ConvertVariant(variant);
// Assert
Assert.NotNull(result);
- Assert.IsType(result);
- Assert.Equal(expectedValue, result);
+ Assert.IsType(result);
+ Assert.Empty((uint[])result);
}
[Fact]
diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/CompressedJsonValueConverterTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/CompressedJsonValueConverterTests.cs
index 808e0479..cb14f59a 100644
--- a/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/CompressedJsonValueConverterTests.cs
+++ b/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/CompressedJsonValueConverterTests.cs
@@ -2,7 +2,7 @@
// // Licensed under the MIT License.
using EventLogExpert.Eventing.ProviderDatabase;
-using EventLogExpert.Eventing.Tests.TestUtils;
+using EventLogExpert.Eventing.TestUtils;
using System.Text;
using System.Text.Json;
@@ -10,16 +10,6 @@ namespace EventLogExpert.Eventing.Tests.ProviderDatabase;
public sealed class CompressedJsonValueConverterTests
{
- [Fact]
- public void Constructor_ShouldCreateInstance()
- {
- // Act
- var converter = new CompressedJsonValueConverter();
-
- // Assert
- Assert.NotNull(converter);
- }
-
[Fact]
public void ConvertFromCompressedJson_WithEmptyCollection_ShouldReturnEmptyCollection()
{
@@ -147,8 +137,9 @@ public void ConvertToCompressedJson_WithNestedObjects_ShouldRoundTrip()
// Assert
Assert.NotNull(decompressed);
Assert.Equal(originalData.Id, decompressed.Id);
+ Assert.NotNull(originalData.Child);
Assert.NotNull(decompressed.Child);
- Assert.Equal(originalData.Child!.Name, decompressed.Child.Name);
+ Assert.Equal(originalData.Child.Name, decompressed.Child.Name);
Assert.Equal(originalData.Child.Value, decompressed.Child.Value);
Assert.Equal(originalData.Child.Items, decompressed.Child.Items);
}
diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderDetailsMergerTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderDetailsMergerTests.cs
index dd0b8645..78027e3b 100644
--- a/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderDetailsMergerTests.cs
+++ b/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderDetailsMergerTests.cs
@@ -3,7 +3,7 @@
using EventLogExpert.Eventing.ProviderDatabase;
using EventLogExpert.Eventing.Providers;
-using EventLogExpert.Eventing.Tests.TestUtils;
+using EventLogExpert.Eventing.TestUtils;
namespace EventLogExpert.Eventing.Tests.ProviderDatabase;
diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderJsonContextTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderJsonContextTests.cs
index 6bd7ac3a..6a0e43ea 100644
--- a/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderJsonContextTests.cs
+++ b/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderJsonContextTests.cs
@@ -8,8 +8,8 @@ namespace EventLogExpert.Eventing.Tests.ProviderDatabase;
public sealed class ProviderJsonContextTests
{
- public static TheoryData SourceGeneratedTypes => new()
- {
+ public static TheoryData SourceGeneratedTypes =>
+ [
typeof(MessageModel),
typeof(EventModel),
typeof(IReadOnlyList),
@@ -21,7 +21,7 @@ public sealed class ProviderJsonContextTests
typeof(List),
typeof(Dictionary),
typeof(Dictionary),
- };
+ ];
[Theory]
[MemberData(nameof(SourceGeneratedTypes))]
@@ -30,7 +30,7 @@ public void Default_ShouldProvideMetadataFor_ProviderDtoType(Type type)
var typeInfo = ProviderJsonContext.Default.GetTypeInfo(type);
Assert.NotNull(typeInfo);
- Assert.Equal(type, typeInfo!.Type);
+ Assert.Equal(type, typeInfo.Type);
}
[Fact]
diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Providers/ProviderDetailsTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Providers/ProviderDetailsTests.cs
index cb5b1279..4d2afd7e 100644
--- a/tests/Unit/EventLogExpert.Eventing.Tests/Providers/ProviderDetailsTests.cs
+++ b/tests/Unit/EventLogExpert.Eventing.Tests/Providers/ProviderDetailsTests.cs
@@ -2,8 +2,8 @@
// // Licensed under the MIT License.
using EventLogExpert.Eventing.Providers;
-using EventLogExpert.Eventing.Tests.TestUtils;
-using EventLogExpert.Eventing.Tests.TestUtils.Constants;
+using EventLogExpert.Eventing.TestUtils;
+using EventLogExpert.Eventing.TestUtils.Constants;
namespace EventLogExpert.Eventing.Tests.Providers;
diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventResolverBaseTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventResolverBaseTests.cs
index 5e593671..6f995d9c 100644
--- a/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventResolverBaseTests.cs
+++ b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventResolverBaseTests.cs
@@ -6,8 +6,8 @@
using EventLogExpert.Eventing.Providers;
using EventLogExpert.Eventing.Readers;
using EventLogExpert.Eventing.Resolvers;
-using EventLogExpert.Eventing.Tests.TestUtils;
-using EventLogExpert.Eventing.Tests.TestUtils.Constants;
+using EventLogExpert.Eventing.TestUtils;
+using EventLogExpert.Eventing.TestUtils.Constants;
using NSubstitute;
using System.Collections.Concurrent;
@@ -251,19 +251,19 @@ public void ResolveEvent_WithAmbiguousPrimaryAndSupplementalModernEventTask_Shou
}
[Fact]
- public void ResolveEvent_WithAmbiguousPrimaryLegacyAndSupplementalMatch_ShouldUseSupplementalDescription()
+ public void ResolveEvent_WithAmbiguousPrimaryLegacyAndSupplemental_ShouldAlsoUseSupplementalForKeywordsAndTask()
{
- // Arrange - primary has 2 legacy messages for the same event ID with no LogLink
- // and identical severity (so disambiguation fails). Supplemental has a matching
- // modern event. Should fall back to supplemental's description.
+ // Arrange - regression for the cross-issue case: when primary has ambiguous legacy
+ // messages AND supplemental is loaded for the description fallback, supplemental's
+ // task and keyword metadata must also be visible to ResolveTaskName / GetKeywordsFromBitmask.
var primaryDetails = new ProviderDetails
{
ProviderName = Constants.TestProviderName,
Events = [],
Messages =
[
- new MessageModel { ShortId = 500, RawId = 0x40000500, Text = Constants.PrimaryMessageA, LogLink = null },
- new MessageModel { ShortId = 500, RawId = 0x40000500, Text = Constants.PrimaryMessageB, LogLink = null }
+ new MessageModel { ShortId = 600, RawId = 0x40000600, Text = Constants.PrimaryMessageA, LogLink = null },
+ new MessageModel { ShortId = 600, RawId = 0x40000600, Text = Constants.PrimaryMessageB, LogLink = null }
],
Parameters = [],
Keywords = new Dictionary(),
@@ -278,19 +278,20 @@ public void ResolveEvent_WithAmbiguousPrimaryLegacyAndSupplementalMatch_ShouldUs
[
new EventModel
{
- Id = 500,
+ Id = 600,
Version = 0,
+ Task = 9,
LogName = Constants.ApplicationLogName,
- Description = "Supplemental modern: %1",
+ Description = Constants.SupplementalDescription,
Keywords = [],
- Template = ""
+ Template = Constants.EmptyTemplate
}
],
Messages = [],
Parameters = [],
- Keywords = new Dictionary(),
+ Keywords = new Dictionary { { 0x4, Constants.SupplementalKeyword } },
Opcodes = new Dictionary(),
- Tasks = new Dictionary()
+ Tasks = new Dictionary { { 9, Constants.SupplementalTask } }
};
var resolver = new SupplementalTestResolver([primaryDetails], supplementalDetails);
@@ -298,11 +299,12 @@ public void ResolveEvent_WithAmbiguousPrimaryLegacyAndSupplementalMatch_ShouldUs
var eventRecord = new EventRecord
{
ProviderName = Constants.TestProviderName,
- Id = 500,
+ Id = 600,
Version = 0,
+ Task = 9,
Level = 4,
LogName = Constants.ApplicationLogName,
- Properties = ["resolved"]
+ Keywords = 0x4
};
// Act
@@ -310,23 +312,25 @@ public void ResolveEvent_WithAmbiguousPrimaryLegacyAndSupplementalMatch_ShouldUs
// Assert
Assert.NotNull(displayEvent);
- Assert.Contains("Supplemental modern: resolved", displayEvent.Description);
+ Assert.Contains(Constants.SupplementalDescription, displayEvent.Description);
+ Assert.Equal(Constants.SupplementalTask, displayEvent.TaskCategory);
+ Assert.Contains(Constants.SupplementalKeyword, displayEvent.Keywords);
}
[Fact]
- public void ResolveEvent_WithAmbiguousPrimaryLegacyAndSupplemental_ShouldAlsoUseSupplementalForKeywordsAndTask()
+ public void ResolveEvent_WithAmbiguousPrimaryLegacyAndSupplementalMatch_ShouldUseSupplementalDescription()
{
- // Arrange - regression for the cross-issue case: when primary has ambiguous legacy
- // messages AND supplemental is loaded for the description fallback, supplemental's
- // task and keyword metadata must also be visible to ResolveTaskName / GetKeywordsFromBitmask.
+ // Arrange - primary has 2 legacy messages for the same event ID with no LogLink
+ // and identical severity (so disambiguation fails). Supplemental has a matching
+ // modern event. Should fall back to supplemental's description.
var primaryDetails = new ProviderDetails
{
ProviderName = Constants.TestProviderName,
Events = [],
Messages =
[
- new MessageModel { ShortId = 600, RawId = 0x40000600, Text = Constants.PrimaryMessageA, LogLink = null },
- new MessageModel { ShortId = 600, RawId = 0x40000600, Text = Constants.PrimaryMessageB, LogLink = null }
+ new MessageModel { ShortId = 500, RawId = 0x40000500, Text = Constants.PrimaryMessageA, LogLink = null },
+ new MessageModel { ShortId = 500, RawId = 0x40000500, Text = Constants.PrimaryMessageB, LogLink = null }
],
Parameters = [],
Keywords = new Dictionary(),
@@ -341,20 +345,19 @@ public void ResolveEvent_WithAmbiguousPrimaryLegacyAndSupplemental_ShouldAlsoUse
[
new EventModel
{
- Id = 600,
+ Id = 500,
Version = 0,
- Task = 9,
LogName = Constants.ApplicationLogName,
- Description = Constants.SupplementalDescription,
+ Description = "Supplemental modern: %1",
Keywords = [],
- Template = Constants.EmptyTemplate
+ Template = ""
}
],
Messages = [],
Parameters = [],
- Keywords = new Dictionary { { 0x4, Constants.SupplementalKeyword } },
+ Keywords = new Dictionary(),
Opcodes = new Dictionary(),
- Tasks = new Dictionary { { 9, Constants.SupplementalTask } }
+ Tasks = new Dictionary()
};
var resolver = new SupplementalTestResolver([primaryDetails], supplementalDetails);
@@ -362,12 +365,11 @@ public void ResolveEvent_WithAmbiguousPrimaryLegacyAndSupplemental_ShouldAlsoUse
var eventRecord = new EventRecord
{
ProviderName = Constants.TestProviderName,
- Id = 600,
+ Id = 500,
Version = 0,
- Task = 9,
Level = 4,
LogName = Constants.ApplicationLogName,
- Keywords = 0x4
+ Properties = ["resolved"]
};
// Act
@@ -375,9 +377,7 @@ public void ResolveEvent_WithAmbiguousPrimaryLegacyAndSupplemental_ShouldAlsoUse
// Assert
Assert.NotNull(displayEvent);
- Assert.Contains(Constants.SupplementalDescription, displayEvent.Description);
- Assert.Equal(Constants.SupplementalTask, displayEvent.TaskCategory);
- Assert.Contains(Constants.SupplementalKeyword, displayEvent.Keywords);
+ Assert.Contains("Supplemental modern: resolved", displayEvent.Description);
}
[Fact]
@@ -496,8 +496,9 @@ public void ResolveEvent_WithClassicEventIdMatchingWin32ErrorCode_ShouldNotPrepe
// Compare against the locale's system-message text for error 2 so the test
// catches false matches on non-English Windows too.
var win32ErrorTwoMessage = NativeMethods.FormatSystemMessage(2);
+ Assert.NotNull(win32ErrorTwoMessage);
Assert.False(string.IsNullOrWhiteSpace(win32ErrorTwoMessage), "FormatSystemMessage(2) must return text on Windows.");
- Assert.DoesNotContain(win32ErrorTwoMessage!, displayEvent.Description);
+ Assert.DoesNotContain(win32ErrorTwoMessage, displayEvent.Description);
// Property tail is still rendered
Assert.Contains("The following information was included with the event:", displayEvent.Description);
Assert.Contains("payload-a", displayEvent.Description);
@@ -539,8 +540,9 @@ public void ResolveEvent_WithClassicEventIdZeroAndNoProviderMetadata_ShouldPrepe
// Compare against the locale's system-message text rather than a hard-coded English
// string so the test passes on non-English Windows.
var expectedSystemMessage = NativeMethods.FormatSystemMessage(0);
+ Assert.NotNull(expectedSystemMessage);
Assert.False(string.IsNullOrWhiteSpace(expectedSystemMessage), "FormatSystemMessage(0) must return text on Windows.");
- Assert.StartsWith(expectedSystemMessage!, displayEvent.Description);
+ Assert.StartsWith(expectedSystemMessage, displayEvent.Description);
Assert.Contains("The following information was included with the event:", displayEvent.Description);
Assert.Contains("AsusUpdateCheck", displayEvent.Description);
Assert.Contains("CServiceControl in OnStop", displayEvent.Description);
@@ -595,44 +597,6 @@ public void ResolveEvent_WithDataSourceTag_ShouldNotMatchAsDataElement()
Assert.DoesNotContain("Failed to resolve", displayEvent.Description);
}
- [Fact]
- public void ResolveEvent_WithEmptyPrimaryAndNoSupplemental_ShouldReturnEventDataTail()
- {
- // Arrange - empty primary AND no supplemental should fall through to the
- // no-metadata fallback. With multiple properties present, that surfaces them
- // under the "included with the event" header (mmc-style payload dump).
- var primaryDetails = new ProviderDetails
- {
- ProviderName = Constants.TestProviderName,
- Events = [],
- Messages = [],
- Parameters = [],
- Keywords = new Dictionary(),
- Opcodes = new Dictionary(),
- Tasks = new Dictionary()
- };
-
- var resolver = new SupplementalTestResolver([primaryDetails], supplemental: null);
-
- var eventRecord = new EventRecord
- {
- ProviderName = Constants.TestProviderName,
- Id = 9999,
- Properties = ["a", "b", "c"]
- };
-
- // Act
- var displayEvent = resolver.ResolveEvent(eventRecord);
-
- // Assert
- Assert.NotNull(displayEvent);
- Assert.Contains("The following information was included with the event:", displayEvent.Description);
- Assert.Contains("a", displayEvent.Description);
- Assert.Contains("b", displayEvent.Description);
- Assert.Contains("c", displayEvent.Description);
- Assert.DoesNotContain("No matching message", displayEvent.Description);
- }
-
[Fact]
public void ResolveEvent_WithEmptyPrimaryAndNonMatchingSupplemental_ShouldReturnNoMatchingMessage()
{
@@ -692,6 +656,44 @@ public void ResolveEvent_WithEmptyPrimaryAndNonMatchingSupplemental_ShouldReturn
Assert.DoesNotContain("Failed to resolve", displayEvent.Description);
}
+ [Fact]
+ public void ResolveEvent_WithEmptyPrimaryAndNoSupplemental_ShouldReturnEventDataTail()
+ {
+ // Arrange - empty primary AND no supplemental should fall through to the
+ // no-metadata fallback. With multiple properties present, that surfaces them
+ // under the "included with the event" header (mmc-style payload dump).
+ var primaryDetails = new ProviderDetails
+ {
+ ProviderName = Constants.TestProviderName,
+ Events = [],
+ Messages = [],
+ Parameters = [],
+ Keywords = new Dictionary(),
+ Opcodes = new Dictionary(),
+ Tasks = new Dictionary()
+ };
+
+ var resolver = new SupplementalTestResolver([primaryDetails], supplemental: null);
+
+ var eventRecord = new EventRecord
+ {
+ ProviderName = Constants.TestProviderName,
+ Id = 9999,
+ Properties = ["a", "b", "c"]
+ };
+
+ // Act
+ var displayEvent = resolver.ResolveEvent(eventRecord);
+
+ // Assert
+ Assert.NotNull(displayEvent);
+ Assert.Contains("The following information was included with the event:", displayEvent.Description);
+ Assert.Contains("a", displayEvent.Description);
+ Assert.Contains("b", displayEvent.Description);
+ Assert.Contains("c", displayEvent.Description);
+ Assert.DoesNotContain("No matching message", displayEvent.Description);
+ }
+
[Fact]
public void ResolveEvent_WithEmptyPrimaryAndSupplementalModernDescription_ShouldUsePrimaryParametersFallback()
{
@@ -961,26 +963,6 @@ public void ResolveEvent_WithFormattingCharacters_ShouldCleanupDescription()
Assert.Contains("\t", displayEvent.Description);
}
- [Fact]
- public void ResolveEvent_WithHResultOutType_ShouldResolveDynamically()
- {
- // Arrange - win:HResult with a common error code should resolve dynamically
- var (details, eventRecord) = EventUtils.CreateModernEvent(
- description: "Error: %1",
- template: """""",
- properties: [unchecked((int)0x80070005)]);
-
- var resolver = new TestEventResolver([details]);
-
- // Act
- var displayEvent = resolver.ResolveEvent(eventRecord);
-
- // Assert - 0x80070005 is ACCESS_DENIED; resolved message should not contain the raw hex code
- Assert.NotNull(displayEvent);
- Assert.DoesNotContain("0x80070005", displayEvent.Description);
- Assert.DoesNotContain("0x00000005", displayEvent.Description);
- }
-
[Fact]
public void ResolveEvent_WithHexOutTypeAfterMissingOutType_ShouldFormatCorrectly()
{
@@ -1086,6 +1068,26 @@ public void ResolveEvent_WithHiddenLengthField_ShouldAlignOutTypesCorrectly()
Assert.Contains("TestName", displayEvent.Description);
}
+ [Fact]
+ public void ResolveEvent_WithHResultOutType_ShouldResolveDynamically()
+ {
+ // Arrange - win:HResult with a common error code should resolve dynamically
+ var (details, eventRecord) = EventUtils.CreateModernEvent(
+ description: "Error: %1",
+ template: """""",
+ properties: [unchecked((int)0x80070005)]);
+
+ var resolver = new TestEventResolver([details]);
+
+ // Act
+ var displayEvent = resolver.ResolveEvent(eventRecord);
+
+ // Assert - 0x80070005 is ACCESS_DENIED; resolved message should not contain the raw hex code
+ Assert.NotNull(displayEvent);
+ Assert.DoesNotContain("0x80070005", displayEvent.Description);
+ Assert.DoesNotContain("0x00000005", displayEvent.Description);
+ }
+
[Fact]
public void ResolveEvent_WithLargeTemplate_ShouldNotOverflowStack()
{
@@ -1763,59 +1765,6 @@ public void ResolveEvent_WithMultipleDataNodesOnOneLine_ShouldCountAllElements()
Assert.DoesNotContain("Failed to resolve", displayEvent.Description);
}
- [Fact]
- public void ResolveEvent_WithMultipleLegacyMessagesSameSeverity_ShouldDisambiguateByQualifier()
- {
- // Arrange - reproduces the Microsoft-Windows-Defrag EventID 258 case where two
- // messages share the same EventId and severity but differ in their high 16 bits
- // (Qualifier). Windows identifies the right entry by full message ID, so
- // RawId == (Qualifiers << 16) | EventId.
- var providerDetails = new ProviderDetails
- {
- ProviderName = Constants.TestProviderName,
- Events = [],
- Messages =
- [
- new MessageModel
- {
- ProviderName = Constants.TestProviderName,
- ShortId = 258,
- RawId = 0x00000102, // Qualifier=0, severity 00=Success
- Text = "The storage optimizer successfully completed %1 on %2"
- },
- new MessageModel
- {
- ProviderName = Constants.TestProviderName,
- ShortId = 258,
- RawId = 0x09000102, // Qualifier=0x900, severity 00=Success
- Text = "The retrim operation was skipped"
- }
- ],
- Parameters = [],
- Keywords = new Dictionary(),
- Tasks = new Dictionary()
- };
-
- var resolver = new TestEventResolver([providerDetails]);
-
- var eventRecord = new EventRecord
- {
- ProviderName = Constants.TestProviderName,
- Id = 258,
- Qualifiers = 0,
- Level = 0,
- LogName = Constants.ApplicationLogName,
- Properties = ["defragmentation", "Data (E:)"]
- };
-
- // Act
- var displayEvent = resolver.ResolveEvent(eventRecord);
-
- // Assert
- Assert.NotNull(displayEvent);
- Assert.Equal("The storage optimizer successfully completed defragmentation on Data (E:)", displayEvent.Description);
- }
-
[Fact]
public void ResolveEvent_WithMultipleLegacyMessages_ShouldDisambiguateByLogLink()
{
@@ -1951,6 +1900,59 @@ public void ResolveEvent_WithMultipleLegacyMessages_ShouldReturnDefaultDescripti
Assert.Contains("No matching message", displayEvent.Description);
}
+ [Fact]
+ public void ResolveEvent_WithMultipleLegacyMessagesSameSeverity_ShouldDisambiguateByQualifier()
+ {
+ // Arrange - reproduces the Microsoft-Windows-Defrag EventID 258 case where two
+ // messages share the same EventId and severity but differ in their high 16 bits
+ // (Qualifier). Windows identifies the right entry by full message ID, so
+ // RawId == (Qualifiers << 16) | EventId.
+ var providerDetails = new ProviderDetails
+ {
+ ProviderName = Constants.TestProviderName,
+ Events = [],
+ Messages =
+ [
+ new MessageModel
+ {
+ ProviderName = Constants.TestProviderName,
+ ShortId = 258,
+ RawId = 0x00000102, // Qualifier=0, severity 00=Success
+ Text = "The storage optimizer successfully completed %1 on %2"
+ },
+ new MessageModel
+ {
+ ProviderName = Constants.TestProviderName,
+ ShortId = 258,
+ RawId = 0x09000102, // Qualifier=0x900, severity 00=Success
+ Text = "The retrim operation was skipped"
+ }
+ ],
+ Parameters = [],
+ Keywords = new Dictionary(),
+ Tasks = new Dictionary()
+ };
+
+ var resolver = new TestEventResolver([providerDetails]);
+
+ var eventRecord = new EventRecord
+ {
+ ProviderName = Constants.TestProviderName,
+ Id = 258,
+ Qualifiers = 0,
+ Level = 0,
+ LogName = Constants.ApplicationLogName,
+ Properties = ["defragmentation", "Data (E:)"]
+ };
+
+ // Act
+ var displayEvent = resolver.ResolveEvent(eventRecord);
+
+ // Assert
+ Assert.NotNull(displayEvent);
+ Assert.Equal("The storage optimizer successfully completed defragmentation on Data (E:)", displayEvent.Description);
+ }
+
[Fact]
public void ResolveEvent_WithMultipleLengthPrefixedBinaryPairs_ShouldMatchTemplate()
{
@@ -2052,29 +2054,6 @@ public void ResolveEvent_WithNewlineSeparatedDataAttributes_ShouldCountAllElemen
Assert.DoesNotContain("Failed to resolve", displayEvent.Description);
}
- [Fact]
- public void ResolveEvent_WithNoProviderDetails_ShouldReturnDefaultDescription()
- {
- // Arrange
- var resolver = new TestEventResolver();
-
- var eventRecord = new EventRecord
- {
- ProviderName = Constants.NonExistentProviderName,
- Id = 1000,
- ComputerName = Constants.TestComputer,
- LogName = Constants.ApplicationLogName
- };
-
- // Act
- resolver.LoadProviderDetails(eventRecord);
- var displayEvent = resolver.ResolveEvent(eventRecord);
-
- // Assert
- Assert.NotNull(displayEvent);
- Assert.Contains("No matching provider", displayEvent.Description);
- }
-
[Fact]
public void ResolveEvent_WithNonClassicEventIdZeroAndNoProviderMetadata_ShouldNotPrependSystemMessage()
{
@@ -2118,6 +2097,29 @@ public void ResolveEvent_WithNonClassicEventIdZeroAndNoProviderMetadata_ShouldNo
Assert.Contains("payload-a", displayEvent.Description);
}
+ [Fact]
+ public void ResolveEvent_WithNoProviderDetails_ShouldReturnDefaultDescription()
+ {
+ // Arrange
+ var resolver = new TestEventResolver();
+
+ var eventRecord = new EventRecord
+ {
+ ProviderName = Constants.NonExistentProviderName,
+ Id = 1000,
+ ComputerName = Constants.TestComputer,
+ LogName = Constants.ApplicationLogName
+ };
+
+ // Act
+ resolver.LoadProviderDetails(eventRecord);
+ var displayEvent = resolver.ResolveEvent(eventRecord);
+
+ // Assert
+ Assert.NotNull(displayEvent);
+ Assert.Contains("No matching provider", displayEvent.Description);
+ }
+
[Fact]
public void ResolveEvent_WithNormalMismatch_ShouldStillReject()
{
@@ -2808,9 +2810,9 @@ public void ResolveEvent_WithProviderHavingOnlyKeywordsTasksOpcodes_ShouldReturn
}
[Fact]
- public void ResolveEvent_WithProviderKeywordsInBits32To47_ShouldResolveKeywords()
+ public void ResolveEvent_WithProviderKeywords_ShouldResolveKeywords()
{
- // Arrange - keyword in bit 32 (0x100000000), which was previously masked out
+ // Arrange
var providerDetails = new ProviderDetails
{
ProviderName = Constants.TestProviderName,
@@ -2819,8 +2821,8 @@ public void ResolveEvent_WithProviderKeywordsInBits32To47_ShouldResolveKeywords(
Parameters = [],
Keywords = new Dictionary
{
- { 0x100000000L, "HighBitKeyword" },
- { 0x1, "LowBitKeyword" }
+ { 0x1, "CustomKeyword1" },
+ { 0x2, "CustomKeyword2" }
},
Tasks = new Dictionary()
};
@@ -2831,7 +2833,7 @@ public void ResolveEvent_WithProviderKeywordsInBits32To47_ShouldResolveKeywords(
{
ProviderName = Constants.TestProviderName,
Id = 1000,
- Keywords = 0x100000001L // Both high and low provider keywords
+ Keywords = 0x3 // Both custom keywords
};
// Act
@@ -2839,14 +2841,14 @@ public void ResolveEvent_WithProviderKeywordsInBits32To47_ShouldResolveKeywords(
// Assert
Assert.NotNull(displayEvent);
- Assert.Contains("HighBitKeyword", displayEvent.Keywords);
- Assert.Contains("LowBitKeyword", displayEvent.Keywords);
+ Assert.Contains("CustomKeyword1", displayEvent.Keywords);
+ Assert.Contains("CustomKeyword2", displayEvent.Keywords);
}
[Fact]
- public void ResolveEvent_WithProviderKeywords_ShouldResolveKeywords()
+ public void ResolveEvent_WithProviderKeywordsInBits32To47_ShouldResolveKeywords()
{
- // Arrange
+ // Arrange - keyword in bit 32 (0x100000000), which was previously masked out
var providerDetails = new ProviderDetails
{
ProviderName = Constants.TestProviderName,
@@ -2855,8 +2857,8 @@ public void ResolveEvent_WithProviderKeywords_ShouldResolveKeywords()
Parameters = [],
Keywords = new Dictionary
{
- { 0x1, "CustomKeyword1" },
- { 0x2, "CustomKeyword2" }
+ { 0x100000000L, "HighBitKeyword" },
+ { 0x1, "LowBitKeyword" }
},
Tasks = new Dictionary()
};
@@ -2867,7 +2869,7 @@ public void ResolveEvent_WithProviderKeywords_ShouldResolveKeywords()
{
ProviderName = Constants.TestProviderName,
Id = 1000,
- Keywords = 0x3 // Both custom keywords
+ Keywords = 0x100000001L // Both high and low provider keywords
};
// Act
@@ -2875,8 +2877,8 @@ public void ResolveEvent_WithProviderKeywords_ShouldResolveKeywords()
// Assert
Assert.NotNull(displayEvent);
- Assert.Contains("CustomKeyword1", displayEvent.Keywords);
- Assert.Contains("CustomKeyword2", displayEvent.Keywords);
+ Assert.Contains("HighBitKeyword", displayEvent.Keywords);
+ Assert.Contains("LowBitKeyword", displayEvent.Keywords);
}
[Fact]
diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventResolverCacheTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventResolverCacheTests.cs
index 43ce5dd6..0677c12e 100644
--- a/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventResolverCacheTests.cs
+++ b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventResolverCacheTests.cs
@@ -7,6 +7,8 @@ namespace EventLogExpert.Eventing.Tests.Resolvers;
public sealed class EventResolverCacheTests
{
+ public enum CacheKind { Description, Value }
+
[Fact]
public void ClearAll_AfterAddingItems_ShouldClearBothCaches()
{
@@ -78,25 +80,29 @@ public void ClearAll_WithEmptyCaches_ShouldNotThrow()
Assert.Null(exception);
}
- [Fact]
- public void GetOrAddDescription_CalledMultipleTimes_ShouldReturnSameReference()
+ [Theory]
+ [InlineData(CacheKind.Description)]
+ [InlineData(CacheKind.Value)]
+ public void GetOrAdd_CalledMultipleTimes_ShouldReturnSameReference(CacheKind kind)
{
// Arrange
var cache = new EventResolverCache();
- var description = "Test Description";
+ var input = $"Test {kind}";
// Act
- var result1 = cache.GetOrAddDescription(description);
- var result2 = cache.GetOrAddDescription(description);
- var result3 = cache.GetOrAddDescription(description);
+ var result1 = GetOrAdd(cache, kind, input);
+ var result2 = GetOrAdd(cache, kind, input);
+ var result3 = GetOrAdd(cache, kind, input);
// Assert
Assert.Same(result1, result2);
Assert.Same(result2, result3);
}
- [Fact]
- public void GetOrAddDescription_ConcurrentCalls_ShouldHandleThreadSafely()
+ [Theory]
+ [InlineData(CacheKind.Description)]
+ [InlineData(CacheKind.Value)]
+ public void GetOrAdd_ConcurrentCalls_ShouldHandleThreadSafely(CacheKind kind)
{
// Arrange
var cache = new EventResolverCache();
@@ -105,159 +111,76 @@ public void GetOrAddDescription_ConcurrentCalls_ShouldHandleThreadSafely()
// Act
Parallel.For(0, 100, i =>
{
- results[i] = cache.GetOrAddDescription($"Description{i % 10}");
+ results[i] = GetOrAdd(cache, kind, $"{kind}{i % 10}");
});
// Assert
- // Verify that same descriptions share the same reference
- for (int i = 0; i < 100; i += 10)
+ // 100 calls were spread across 10 distinct keys ("{kind}0" .. "{kind}9"), 10 calls per key.
+ // For each key, every occurrence must be the same reference (de-dup contract under
+ // contention). The previous loop only stepped i by 10, which meant i % 10 was always 0
+ // and only key "{kind}0" was actually validated; iterating each key 0..9 explicitly
+ // covers all ten distinct cache slots so a regression in any one of them fails the test.
+ for (int key = 0; key < 10; key++)
{
- var description = $"Description{i % 10}";
- var firstOccurrence = results[i];
+ var expected = results[key];
+ Assert.NotNull(expected);
- for (int j = i; j < 100; j += 10)
+ for (int occurrence = 1; occurrence < 10; occurrence++)
{
- Assert.Same(firstOccurrence, results[j]);
+ Assert.Same(expected, results[key + (occurrence * 10)]);
}
}
}
- [Fact]
- public void GetOrAddDescription_WithDifferentDescriptions_ShouldReturnDifferentReferences()
+ [Theory]
+ [InlineData(CacheKind.Description, "Description 1", "Description 2")]
+ [InlineData(CacheKind.Value, "Value 1", "Value 2")]
+ public void GetOrAdd_WithDifferentInputs_ShouldReturnDifferentReferences(CacheKind kind, string a, string b)
{
// Arrange
var cache = new EventResolverCache();
- var description1 = "Description 1";
- var description2 = "Description 2";
// Act
- var result1 = cache.GetOrAddDescription(description1);
- var result2 = cache.GetOrAddDescription(description2);
+ var result1 = GetOrAdd(cache, kind, a);
+ var result2 = GetOrAdd(cache, kind, b);
// Assert
Assert.NotSame(result1, result2);
- Assert.Equal("Description 1", result1);
- Assert.Equal("Description 2", result2);
+ Assert.Equal(a, result1);
+ Assert.Equal(b, result2);
}
- [Fact]
- public void GetOrAddDescription_WithEmptyString_ShouldReturnSameEmptyStringReference()
+ [Theory]
+ [InlineData(CacheKind.Description)]
+ [InlineData(CacheKind.Value)]
+ public void GetOrAdd_WithEmptyString_ShouldReturnSameEmptyStringReference(CacheKind kind)
{
// Arrange
var cache = new EventResolverCache();
// Act
- var result1 = cache.GetOrAddDescription(string.Empty);
- var result2 = cache.GetOrAddDescription(string.Empty);
+ var result1 = GetOrAdd(cache, kind, string.Empty);
+ var result2 = GetOrAdd(cache, kind, string.Empty);
// Assert
Assert.Same(result1, result2);
Assert.Equal(string.Empty, result1);
}
- [Fact]
- public void GetOrAddDescription_WithNewString_ShouldAddToCache()
- {
- // Arrange
- var cache = new EventResolverCache();
- var description = new string("Test".ToCharArray()); // Force new string instance
-
- // Act
- var result = cache.GetOrAddDescription(description);
-
- // Assert
- Assert.Equal(description, result);
- }
-
- [Fact]
- public void GetOrAddValue_CalledMultipleTimes_ShouldReturnSameReference()
+ [Theory]
+ [InlineData(CacheKind.Description)]
+ [InlineData(CacheKind.Value)]
+ public void GetOrAdd_WithNewString_ShouldAddToCache(CacheKind kind)
{
// Arrange
var cache = new EventResolverCache();
- var value = "Test Value";
+ var input = new string("Test".ToCharArray()); // Force new string instance
// Act
- var result1 = cache.GetOrAddValue(value);
- var result2 = cache.GetOrAddValue(value);
- var result3 = cache.GetOrAddValue(value);
+ var result = GetOrAdd(cache, kind, input);
// Assert
- Assert.Same(result1, result2);
- Assert.Same(result2, result3);
- }
-
- [Fact]
- public void GetOrAddValue_ConcurrentCalls_ShouldHandleThreadSafely()
- {
- // Arrange
- var cache = new EventResolverCache();
- var results = new string[100];
-
- // Act
- Parallel.For(0, 100, i =>
- {
- results[i] = cache.GetOrAddValue($"Value{i % 10}");
- });
-
- // Assert
- // Verify that same values share the same reference
- for (int i = 0; i < 100; i += 10)
- {
- var value = $"Value{i % 10}";
- var firstOccurrence = results[i];
-
- for (int j = i; j < 100; j += 10)
- {
- Assert.Same(firstOccurrence, results[j]);
- }
- }
- }
-
- [Fact]
- public void GetOrAddValue_WithDifferentValues_ShouldReturnDifferentReferences()
- {
- // Arrange
- var cache = new EventResolverCache();
- var value1 = "Value 1";
- var value2 = "Value 2";
-
- // Act
- var result1 = cache.GetOrAddValue(value1);
- var result2 = cache.GetOrAddValue(value2);
-
- // Assert
- Assert.NotSame(result1, result2);
- Assert.Equal("Value 1", result1);
- Assert.Equal("Value 2", result2);
- }
-
- [Fact]
- public void GetOrAddValue_WithEmptyString_ShouldReturnSameEmptyStringReference()
- {
- // Arrange
- var cache = new EventResolverCache();
-
- // Act
- var result1 = cache.GetOrAddValue(string.Empty);
- var result2 = cache.GetOrAddValue(string.Empty);
-
- // Assert
- Assert.Same(result1, result2);
- Assert.Equal(string.Empty, result1);
- }
-
- [Fact]
- public void GetOrAddValue_WithNewString_ShouldAddToCache()
- {
- // Arrange
- var cache = new EventResolverCache();
- var value = new string("Test".ToCharArray()); // Force new string instance
-
- // Act
- var result = cache.GetOrAddValue(value);
-
- // Assert
- Assert.Equal(value, result);
+ Assert.Equal(input, result);
}
[Fact]
@@ -338,4 +261,7 @@ public void SeparateCaches_DescriptionAndValue_ShouldNotInterfere()
Assert.Equal(sharedString, description);
Assert.Equal(sharedString, value);
}
+
+ private static string GetOrAdd(EventResolverCache cache, CacheKind kind, string input) =>
+ kind == CacheKind.Description ? cache.GetOrAddDescription(input) : cache.GetOrAddValue(input);
}
diff --git a/tests/Unit/EventLogExpert.UI.Tests/Alerts/AlertDialogServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Alerts/AlertDialogServiceTests.cs
index c9196b5c..cabf6f8a 100644
--- a/tests/Unit/EventLogExpert.UI.Tests/Alerts/AlertDialogServiceTests.cs
+++ b/tests/Unit/EventLogExpert.UI.Tests/Alerts/AlertDialogServiceTests.cs
@@ -97,6 +97,31 @@ public async Task DisplayPrompt_WhenNoActiveHost_ShouldCallStandalonePromptOpene
Assert.Equal("old-value", capturedPrompt["InitialValue"]);
}
+ [Fact]
+ public async Task ShowAlert_ShouldMarshalThroughMainThreadService()
+ {
+ // Arrange — capture that MainThread invocation happens before the routing decision runs.
+ var broker = Substitute.For();
+ broker.TryGet(out Arg.Any()).Returns(false);
+
+ var mainThread = Substitute.For();
+ mainThread.InvokeOnMainThreadAsync(Arg.Any>())
+ .Returns(call => ((Func)call[0])());
+
+ var sut = new AlertDialogService(
+ broker,
+ mainThread,
+ Substitute.For(),
+ _ => Task.FromResult(true),
+ _ => Task.FromResult(string.Empty));
+
+ // Act
+ await sut.ShowAlert("t", "m", "c");
+
+ // Assert
+ await mainThread.Received(1).InvokeOnMainThreadAsync(Arg.Any>());
+ }
+
[Fact]
public async Task ShowAlertOneButton_BannerPresentation_DoesNotMarshalThroughMainThreadService()
{
@@ -383,31 +408,6 @@ public async Task ShowAlertTwoButton_WhenInlineCancelled_ShouldReturnFalse()
Assert.False(result);
}
- [Fact]
- public async Task ShowAlert_ShouldMarshalThroughMainThreadService()
- {
- // Arrange — capture that MainThread invocation happens before the routing decision runs.
- var broker = Substitute.For();
- broker.TryGet(out Arg.Any()).Returns(false);
-
- var mainThread = Substitute.For();
- mainThread.InvokeOnMainThreadAsync(Arg.Any>())
- .Returns(call => ((Func)call[0])());
-
- var sut = new AlertDialogService(
- broker,
- mainThread,
- Substitute.For(),
- _ => Task.FromResult(true),
- _ => Task.FromResult(string.Empty));
-
- // Act
- await sut.ShowAlert("t", "m", "c");
-
- // Assert
- await mainThread.Received(1).InvokeOnMainThreadAsync(Arg.Any>());
- }
-
[Fact]
public async Task ShowErrorAlert_DoesNotMarshalThroughMainThreadService()
{
diff --git a/tests/Unit/EventLogExpert.UI.Tests/Banner/BannerServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Banner/BannerServiceTests.cs
index 964dd579..5cce3af7 100644
--- a/tests/Unit/EventLogExpert.UI.Tests/Banner/BannerServiceTests.cs
+++ b/tests/Unit/EventLogExpert.UI.Tests/Banner/BannerServiceTests.cs
@@ -49,7 +49,9 @@ public void BannerProgressEntry_Cancel_InvokesUnderlyingEventArgsCancel()
new UpgradeBatchStartedEventArgs(batchId, UpgradeProgressScope.Background, 1, cts));
// Act
- sut.BackgroundProgress!.Cancel();
+ var progress = sut.BackgroundProgress;
+ Assert.NotNull(progress);
+ progress.Cancel();
// Assert
Assert.True(cts.IsCancellationRequested);
@@ -892,7 +894,9 @@ public void UpgradeBatchProgress_RefreshesQueuedBatchesAfterFromDatabaseService(
databaseService.UpgradeBatchStarted += Raise.EventWith(
databaseService,
new UpgradeBatchStartedEventArgs(batchId, UpgradeProgressScope.Background, 1, cts));
- Assert.Equal(0, sut.BackgroundProgress!.QueuedBatchesAfter);
+ var initial = sut.BackgroundProgress;
+ Assert.NotNull(initial);
+ Assert.Equal(0, initial.QueuedBatchesAfter);
// Act — another batch enqueues; Progress event picks up the new count.
databaseService.QueuedBatchCount.Returns(3);
@@ -901,7 +905,9 @@ public void UpgradeBatchProgress_RefreshesQueuedBatchesAfterFromDatabaseService(
new UpgradeBatchProgressEventArgs(batchId, 1, "first.db", UpgradePhase.BackingUp));
// Assert
- Assert.Equal(3, sut.BackgroundProgress.QueuedBatchesAfter);
+ var after = sut.BackgroundProgress;
+ Assert.NotNull(after);
+ Assert.Equal(3, after.QueuedBatchesAfter);
}
[Fact]
diff --git a/tests/Unit/EventLogExpert.UI.Tests/EventLog/EffectsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/EventLog/EffectsTests.cs
index 84b5972c..50cf2003 100644
--- a/tests/Unit/EventLogExpert.UI.Tests/EventLog/EffectsTests.cs
+++ b/tests/Unit/EventLogExpert.UI.Tests/EventLog/EffectsTests.cs
@@ -439,7 +439,7 @@ public async Task HandleLoadNewEvents_WhenBufferSpansMultipleLogs_ShouldGroupInt
// Assert: a single batched append covering both logs.
mockDispatcher.Received(1).Dispatch(Arg.Any());
Assert.NotNull(captured);
- Assert.Equal(2, captured!.EventsByLog.Count);
+ Assert.Equal(2, captured.EventsByLog.Count);
Assert.Equal(2, captured.EventsByLog[applicationLog.Id].Count);
Assert.Single(captured.EventsByLog[testLog.Id]);
}
diff --git a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.Database.cs b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.Database.cs
deleted file mode 100644
index 3c7ff8f3..00000000
--- a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.Database.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-// // Copyright (c) Microsoft Corporation.
-// // Licensed under the MIT License.
-
-namespace EventLogExpert.UI.Tests.TestUtils.Constants;
-
-public sealed partial class Constants
-{
- public const string AnotherDb = "AnotherDb";
- // Database names
- public const string DatabaseA = "Database A";
- public const string DatabaseB = "Database B";
- public const string DatabaseC = "Database C";
-
- // Disabled database names
- public const string DisabledDb = "DisabledDb";
- public const string DisabledDb1 = "DisabledDb1";
- public const string DisabledDb2 = "DisabledDb2";
- public const string InitialDisabled = "InitialDisabled";
- public const string Linux1 = "Linux 1";
- public const string NewDisabled1 = "NewDisabled1";
- public const string NewDisabled2 = "NewDisabled2";
- public const string Server1 = "Server 1";
- public const string Server10 = "Server 10";
- public const string Server2 = "Server 2";
- public const string Server20 = "Server 20";
-
- // Non-versioned database names
- public const string SimpleDatabase = "SimpleDatabase";
-
- // Test database file names (EnabledDatabaseCollectionProviderTests)
- public const string TestDb1 = "TestDb1.db";
- public const string TestDb2 = "TestDb2.db";
- public const string TestDb3 = "TestDb3.db";
-
- // Test database full paths
- public const string TestDbPath1 = @"C:\Databases\TestDb1.db";
- public const string TestDbPath2 = @"C:\Databases\TestDb2.db";
- public const string TestDbPath3 = @"C:\Databases\TestDb3.db";
- public const string Windows10 = "Windows 10";
- public const string Windows11 = "Windows 11";
-
- // Versioned database names
- public const string Windows9 = "Windows 9";
-}
diff --git a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.DebugLog.cs b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.DebugLog.cs
index 333ff094..e49df283 100644
--- a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.DebugLog.cs
+++ b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.DebugLog.cs
@@ -5,13 +5,7 @@ namespace EventLogExpert.UI.Tests.TestUtils.Constants;
public sealed partial class Constants
{
- public const string DebugLogDefaultLevelMessage = "Default level message";
- public const string DebugLogErrorMessage = "Error message";
- public const string DebugLogExistingContent = "Existing log content";
public const string DebugLogFirstMessage = "First message";
- public const string DebugLogLine1 = "Line 1";
- public const string DebugLogLine2 = "Line 2";
- public const string DebugLogLine3 = "Line 3";
public const string DebugLogNewMessage = "New message";
public const string DebugLogOlderTimestamp = "2026-04-29T07:53:19.0000000-04:00";
public const string DebugLogSecondMessage = "Second message";