From 8eff3919857e5914e988cedd9a95571517caf5d6 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sat, 9 May 2026 17:35:13 -0500 Subject: [PATCH 1/2] Restructure Eventing into Common/, rename folders/symbols, apply branch-wide hygiene pass --- .../Database/DatabaseRecoveryDialog.razor.cs | 2 +- .../Menu/MenuBar.razor.cs | 12 +- .../CreateDatabaseCommand.cs | 28 +- .../DbToolCommand.cs | 4 +- .../DiffDatabaseCommand.cs | 18 +- .../MergeDatabaseCommand.cs | 38 +- .../MtaProviderSource.cs | 118 +++--- .../ProviderSource.cs | 58 +-- src/EventLogExpert.EventDbTool/ShowCommand.cs | 3 +- .../UpgradeDatabaseCommand.cs | 12 +- .../Common/Channels/LogChannelMethods.cs} | 16 +- .../Channels/LogChannelNames.cs} | 8 +- .../Common/Channels/LogPathType.cs | 10 + .../Common/Databases/DatabasePathSorter.cs | 89 ++++ .../Databases/IActiveDatabasePathsProvider.cs | 11 + .../Events}/DisplayEventModel.cs | 8 +- .../Events}/SeverityLevel.cs | 2 +- .../IDatabaseCollectionProvider.cs | 11 - .../EventResolvers/IEventXmlResolver.cs | 35 -- .../Interop/NativeMethods.Evt.cs | 6 +- .../Logging/ITraceLogger.cs | 10 +- ...LogHandler.cs => InformationLogHandler.cs} | 4 +- .../Logging/TraceLogger.cs | 10 +- ...WarnLogHandler.cs => WarningLogHandler.cs} | 4 +- .../CompressedJsonValueConverter.cs | 20 +- .../DatabaseUpgradeException.cs | 2 +- .../ProviderDatabaseSchemaState.cs | 2 +- .../ProviderDatabaseSchemaVersion.cs | 2 +- .../ProviderDbContext.cs} | 23 +- .../ProviderDetailsMerger.cs | 5 +- .../ProviderJsonContext.cs | 4 +- .../ProviderJsonSerializerOptions.cs | 3 +- .../SchemaStateMessages.cs} | 10 +- .../Providers/EventMessageProvider.cs | 131 +++--- .../{Models => Providers}/EventMetadata.cs | 4 +- .../{Models => Providers}/EventModel.cs | 14 +- .../{Models => Providers}/MessageModel.cs | 2 +- .../Providers/ProviderDetails.cs | 2 - .../Providers/ProviderMetadata.cs | 1 - .../Providers/RegistryProvider.cs | 9 +- .../Readers/EventLogInformation.cs | 23 +- .../Readers/EventLogReader.cs | 6 +- .../Readers/EventLogSession.cs | 3 +- .../Readers/EventLogWatcher.cs | 28 +- .../{Models => Readers}/EventRecord.cs | 6 +- .../Readers/PathType.cs | 10 - .../EventResolver.cs | 89 +--- .../EventResolverBase.cs | 12 +- .../EventResolverCache.cs | 2 +- .../EventXmlResolver.cs | 30 +- .../IEventResolver.cs | 9 +- .../IEventResolverCache.cs | 2 +- .../Resolvers/IEventXmlResolver.cs | 34 ++ .../SeverityFormatter.cs} | 12 +- src/EventLogExpert.UI/FilterMethods.cs | 3 +- .../Interfaces/IFilterService.cs | 2 +- .../Models/CompiledFilter.cs | 10 +- src/EventLogExpert.UI/Models/EventFilter.cs | 2 +- src/EventLogExpert.UI/Models/EventLogData.cs | 6 +- .../Models/EventTableModel.cs | 4 +- .../Services/ApplicationRestartService.cs | 2 +- .../Services/BannerService.cs | 2 +- .../Services/DatabaseService.cs | 111 ++--- .../Services/DebugLogService.cs | 76 ++-- .../Services/FilterCategoryItemsCache.cs | 6 +- .../Services/FilterCompiler.cs | 2 +- .../Services/FilterService.cs | 2 +- .../Services/UpdateService.cs | 2 +- .../Store/EventLog/EventLogAction.cs | 6 +- .../Store/EventLog/EventLogEffects.cs | 29 +- .../Store/EventLog/EventLogReducers.cs | 46 +-- .../Store/EventLog/EventLogState.cs | 2 +- .../Store/EventLog/ILogReloadCoordinator.cs | 4 +- ...WatcherService.cs => LogWatcherService.cs} | 18 +- .../Store/EventTable/EventTableAction.cs | 2 +- .../Store/EventTable/EventTableReducers.cs | 8 +- .../Store/EventTable/EventTableState.cs | 2 +- .../Store/LoggingMiddleware.cs | 3 +- .../Components/Sections/DetailsPane.razor.cs | 5 +- .../Components/Sections/EventTable.razor | 3 +- .../Components/Sections/EventTable.razor.cs | 34 +- .../Sections/SplitLogTabPane.razor.cs | 34 +- .../Components/Sections/StatusBar.razor | 5 +- src/EventLogExpert/MainPage.xaml.cs | 6 +- src/EventLogExpert/MauiProgram.cs | 9 +- .../Services/ClipboardService.cs | 4 +- .../Services/MauiMenuActionService.cs | 22 +- .../EventMessageProviderIntegrationTests.cs | 116 +++++- .../Providers/RegistryProviderTests.cs | 44 +- .../Readers/EventLogInformationTests.cs | 55 +-- .../Readers/EventLogReaderTests.cs | 127 +++--- .../Readers/EventLogSessionTests.cs | 113 +++--- .../Readers/EventLogWatcherTests.cs | 100 ++--- .../Readers/SmallEvtxFixture.cs | 3 +- .../DiffDatabaseCommandTests.cs | 4 +- .../TestUtils/DatabaseTestUtils.cs | 4 +- .../UpgradeDatabaseCommandTests.cs | 8 +- .../Channels/LogChannelMethodsTests.cs} | 58 +-- .../Common/Channels/LogChannelNamesTests.cs | 36 ++ .../Databases/DatabasePathSorterTests.cs | 151 +++++++ .../CompressedJsonValueConverterTests.cs | 4 +- .../ProviderDbContextTests.cs} | 159 ++++---- .../ProviderDetailsMergerTests.cs | 5 +- .../ProviderJsonContextTests.cs | 42 +- .../Providers/EventMessageProviderTests.cs | 36 +- .../Providers/ProviderDetailsTests.cs | 1 - .../Readers/LogNamesTests.cs | 36 -- ...EventProviderDatabaseEventResolverTests.cs | 308 ++++---------- .../EventResolverBaseTests.cs | 380 +++++++++--------- .../EventResolverCacheTests.cs | 4 +- .../EventXmlResolverTests.cs | 24 +- .../LocalProviderEventResolverTests.cs | 106 ++--- .../VersatileEventResolverTests.cs | 66 +-- .../TestUtils/EventUtils.cs | 2 +- .../DateRangeDefaultsTests.cs | 10 +- .../FilterMethodsTests.cs | 302 +++++++------- .../Services/BannerServiceTests.cs | 2 +- .../Services/DatabaseServiceTests.cs | 76 ++-- .../Services/DebugLogServiceTests.cs | 104 ++--- .../Services/FilterCategoryItemsCacheTests.cs | 46 +-- .../Services/FilterServiceTests.cs | 254 ++++++------ .../Store/EventLog/EventLogEffectsTests.cs | 267 ++++++------ .../Store/EventLog/EventLogStoreTests.cs | 204 +++++----- .../Store/EventTable/EventTableStoreTests.cs | 288 ++++++------- .../FilterPane/FilterPaneEffectsTests.cs | 108 ++--- .../TestUtils/DatabaseSeedUtils.cs | 4 +- .../TestUtils/EventUtils.cs | 6 +- .../TestUtils/LoggerUtils.cs | 4 +- 128 files changed, 2616 insertions(+), 2540 deletions(-) rename src/{EventLogExpert.UI/LogNameMethods.cs => EventLogExpert.Eventing/Common/Channels/LogChannelMethods.cs} (77%) rename src/EventLogExpert.Eventing/{Readers/LogNames.cs => Common/Channels/LogChannelNames.cs} (62%) create mode 100644 src/EventLogExpert.Eventing/Common/Channels/LogPathType.cs create mode 100644 src/EventLogExpert.Eventing/Common/Databases/DatabasePathSorter.cs create mode 100644 src/EventLogExpert.Eventing/Common/Databases/IActiveDatabasePathsProvider.cs rename src/EventLogExpert.Eventing/{Models => Common/Events}/DisplayEventModel.cs (88%) rename src/EventLogExpert.Eventing/{Models => Common/Events}/SeverityLevel.cs (85%) delete mode 100644 src/EventLogExpert.Eventing/EventResolvers/IDatabaseCollectionProvider.cs delete mode 100644 src/EventLogExpert.Eventing/EventResolvers/IEventXmlResolver.cs rename src/EventLogExpert.Eventing/Logging/{InfoLogHandler.cs => InformationLogHandler.cs} (91%) rename src/EventLogExpert.Eventing/Logging/{WarnLogHandler.cs => WarningLogHandler.cs} (91%) rename src/EventLogExpert.Eventing/{EventProviderDatabase => ProviderDatabase}/CompressedJsonValueConverter.cs (95%) rename src/EventLogExpert.Eventing/{EventProviderDatabase => ProviderDatabase}/DatabaseUpgradeException.cs (92%) rename src/EventLogExpert.Eventing/{EventProviderDatabase => ProviderDatabase}/ProviderDatabaseSchemaState.cs (81%) rename src/EventLogExpert.Eventing/{EventProviderDatabase => ProviderDatabase}/ProviderDatabaseSchemaVersion.cs (78%) rename src/EventLogExpert.Eventing/{EventProviderDatabase/EventProviderDbContext.cs => ProviderDatabase/ProviderDbContext.cs} (91%) rename src/EventLogExpert.Eventing/{EventProviderDatabase => ProviderDatabase}/ProviderDetailsMerger.cs (98%) rename src/EventLogExpert.Eventing/{EventProviderDatabase => ProviderDatabase}/ProviderJsonContext.cs (89%) rename src/EventLogExpert.Eventing/{EventProviderDatabase => ProviderDatabase}/ProviderJsonSerializerOptions.cs (90%) rename src/EventLogExpert.Eventing/{EventProviderDatabase/DatabaseSchemaMessages.cs => ProviderDatabase/SchemaStateMessages.cs} (80%) rename src/EventLogExpert.Eventing/{Models => Providers}/EventMetadata.cs (95%) rename src/EventLogExpert.Eventing/{Models => Providers}/EventModel.cs (91%) rename src/EventLogExpert.Eventing/{Models => Providers}/MessageModel.cs (97%) rename src/EventLogExpert.Eventing/{Models => Readers}/EventRecord.cs (88%) delete mode 100644 src/EventLogExpert.Eventing/Readers/PathType.cs rename src/EventLogExpert.Eventing/{EventResolvers => Resolvers}/EventResolver.cs (74%) rename src/EventLogExpert.Eventing/{EventResolvers => Resolvers}/EventResolverBase.cs (99%) rename src/EventLogExpert.Eventing/{EventResolvers => Resolvers}/EventResolverCache.cs (95%) rename src/EventLogExpert.Eventing/{EventResolvers => Resolvers}/EventXmlResolver.cs (85%) rename src/EventLogExpert.Eventing/{EventResolvers => Resolvers}/IEventResolver.cs (65%) rename src/EventLogExpert.Eventing/{EventResolvers => Resolvers}/IEventResolverCache.cs (82%) create mode 100644 src/EventLogExpert.Eventing/Resolvers/IEventXmlResolver.cs rename src/EventLogExpert.Eventing/{Models/Severity.cs => Resolvers/SeverityFormatter.cs} (61%) rename src/EventLogExpert.UI/Store/EventLog/{LiveLogWatcherService.cs => LogWatcherService.cs} (90%) rename tests/Unit/{EventLogExpert.UI.Tests/LogNameMethodsTests.cs => EventLogExpert.Eventing.Tests/Common/Channels/LogChannelMethodsTests.cs} (60%) create mode 100644 tests/Unit/EventLogExpert.Eventing.Tests/Common/Channels/LogChannelNamesTests.cs create mode 100644 tests/Unit/EventLogExpert.Eventing.Tests/Common/Databases/DatabasePathSorterTests.cs rename tests/Unit/EventLogExpert.Eventing.Tests/{EventProviderDatabase => ProviderDatabase}/CompressedJsonValueConverterTests.cs (98%) rename tests/Unit/EventLogExpert.Eventing.Tests/{EventProviderDatabase/EventProviderDbContextTests.cs => ProviderDatabase/ProviderDbContextTests.cs} (90%) rename tests/Unit/EventLogExpert.Eventing.Tests/{EventProviderDatabase => ProviderDatabase}/ProviderDetailsMergerTests.cs (98%) rename tests/Unit/EventLogExpert.Eventing.Tests/{EventProviderDatabase => ProviderDatabase}/ProviderJsonContextTests.cs (94%) delete mode 100644 tests/Unit/EventLogExpert.Eventing.Tests/Readers/LogNamesTests.cs rename tests/Unit/EventLogExpert.Eventing.Tests/{EventResolvers => Resolvers}/EventProviderDatabaseEventResolverTests.cs (67%) rename tests/Unit/EventLogExpert.Eventing.Tests/{EventResolvers => Resolvers}/EventResolverBaseTests.cs (99%) rename tests/Unit/EventLogExpert.Eventing.Tests/{EventResolvers => Resolvers}/EventResolverCacheTests.cs (98%) rename tests/Unit/EventLogExpert.Eventing.Tests/{EventResolvers => Resolvers}/EventXmlResolverTests.cs (93%) rename tests/Unit/EventLogExpert.Eventing.Tests/{EventResolvers => Resolvers}/LocalProviderEventResolverTests.cs (83%) rename tests/Unit/EventLogExpert.Eventing.Tests/{EventResolvers => Resolvers}/VersatileEventResolverTests.cs (87%) diff --git a/src/EventLogExpert.Components/Database/DatabaseRecoveryDialog.razor.cs b/src/EventLogExpert.Components/Database/DatabaseRecoveryDialog.razor.cs index c25a097d..ef01a0b0 100644 --- a/src/EventLogExpert.Components/Database/DatabaseRecoveryDialog.razor.cs +++ b/src/EventLogExpert.Components/Database/DatabaseRecoveryDialog.razor.cs @@ -106,7 +106,7 @@ private async Task ApplyAsync() } catch (InvalidOperationException invalidOperation) { - TraceLogger.Warn( + TraceLogger.Warning( $"DatabaseRecoveryDialog: skipped '{fileName}' ({action}) — {invalidOperation.Message}"); continue; } diff --git a/src/EventLogExpert.Components/Menu/MenuBar.razor.cs b/src/EventLogExpert.Components/Menu/MenuBar.razor.cs index 835b0317..670c52b0 100644 --- a/src/EventLogExpert.Components/Menu/MenuBar.razor.cs +++ b/src/EventLogExpert.Components/Menu/MenuBar.razor.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; @@ -129,9 +129,9 @@ private IReadOnlyList BuildOpenSubMenu(bool combineLog) MenuItem.Item("Folder", () => Actions.OpenFolderAsync(combineLog)), MenuItem.SubMenu("Live", [ - MenuItem.Item(LogNames.ApplicationLog, () => Actions.OpenLiveLogAsync(LogNames.ApplicationLog, combineLog)), - MenuItem.Item(LogNames.SystemLog, () => Actions.OpenLiveLogAsync(LogNames.SystemLog, combineLog)), - MenuItem.Item(LogNames.SecurityLog, () => Actions.OpenLiveLogAsync(LogNames.SecurityLog, combineLog), isEnabled: isAdmin), + MenuItem.Item(LogChannelNames.ApplicationLog, () => Actions.OpenLiveLogAsync(LogChannelNames.ApplicationLog, combineLog)), + MenuItem.Item(LogChannelNames.SystemLog, () => Actions.OpenLiveLogAsync(LogChannelNames.SystemLog, combineLog)), + MenuItem.Item(LogChannelNames.SecurityLog, () => Actions.OpenLiveLogAsync(LogChannelNames.SecurityLog, combineLog), isEnabled: isAdmin), MenuItem.AsyncSubMenu( "Other Logs", async () => BuildOtherLogsTree(await Actions.GetOtherLogNamesAsync(), combineLog, isAdmin)), @@ -146,12 +146,12 @@ private IReadOnlyList BuildOtherLogsTree(IReadOnlyList logName foreach (var logName in logNames) { - var path = LogNameMethods.GetMenuPath(logName); + var path = LogChannelMethods.GetMenuPath(logName); if (path.Count == 0) { continue; } var log = path[^1]; - var logIsEnabled = isAdmin || !LogNames.AdminOnlyLiveLogNames.Contains(logName); + var logIsEnabled = isAdmin || !LogChannelNames.AdminOnlyLiveChannels.Contains(logName); var logMenuItem = MenuItem.Item(log, () => Actions.OpenLiveLogAsync(logName, combineLog), isEnabled: logIsEnabled); if (path.Count == 1) diff --git a/src/EventLogExpert.EventDbTool/CreateDatabaseCommand.cs b/src/EventLogExpert.EventDbTool/CreateDatabaseCommand.cs index 81e7d2aa..95862cee 100644 --- a/src/EventLogExpert.EventDbTool/CreateDatabaseCommand.cs +++ b/src/EventLogExpert.EventDbTool/CreateDatabaseCommand.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventProviderDatabase; using EventLogExpert.Eventing.Logging; +using EventLogExpert.Eventing.ProviderDatabase; using EventLogExpert.Eventing.Providers; using Microsoft.Extensions.DependencyInjection; using System.CommandLine; @@ -101,7 +101,7 @@ private void CreateDatabase(string path, string? source, string? filter, string? skipProviderNames.Add(name); } - Logger.Info($"Found {skipProviderNames.Count} providers in {skipProvidersInFile}. These will not be included in the new database."); + Logger.Information($"Found {skipProviderNames.Count} providers in {skipProvidersInFile}. These will not be included in the new database."); } // Stream details directly into the DbContext. For .evtx sources, scanning the file @@ -109,15 +109,15 @@ private void CreateDatabase(string path, string? source, string? filter, string? // log header — that would cause a second full pass over the .evtx. Instead we // buffer the first batch of resolved details, derive the header column width from // those names, then log the header + buffered rows and continue streaming. - const int batchSize = 100; + const int BatchSize = 100; var count = 0; var headerLogged = false; - var pendingForHeader = new List(batchSize); + var pendingForHeader = new List(BatchSize); // Defer creating the DbContext (and therefore the .db file on disk) until we have // at least one provider to persist. This prevents leaving an empty database behind // when no provider details could be resolved (e.g., .evtx without LocaleMetaData). - EventProviderDbContext? dbContext = null; + ProviderDbContext? dbContext = null; try { @@ -131,19 +131,19 @@ private void CreateDatabase(string path, string? source, string? filter, string? { pendingForHeader.Add(details); - if (pendingForHeader.Count < batchSize) { continue; } + if (pendingForHeader.Count < BatchSize) { continue; } FlushHeaderAndBuffer(ref dbContext, path, pendingForHeader, ref count); headerLogged = true; continue; } - dbContext ??= new EventProviderDbContext(path, false, Logger); + dbContext ??= new ProviderDbContext(path, false, Logger); dbContext.ProviderDetails.Add(details); LogProviderDetails(details); count++; - if (count % batchSize != 0) { continue; } + if (count % BatchSize != 0) { continue; } dbContext.SaveChanges(); dbContext.ChangeTracker.Clear(); @@ -157,17 +157,17 @@ private void CreateDatabase(string path, string? source, string? filter, string? if (dbContext is null) { - Logger.Warn($"No provider details could be resolved from the source. Database was not created."); + Logger.Warning($"No provider details could be resolved from the source. Database was not created."); return; } - Logger.Info($""); - Logger.Info($"Saving database. Please wait..."); + Logger.Information($""); + Logger.Information($"Saving database. Please wait..."); dbContext.SaveChanges(); - Logger.Info($"Done!"); + Logger.Information($"Done!"); } finally { @@ -181,14 +181,14 @@ private void CreateDatabase(string path, string? source, string? filter, string? } private void FlushHeaderAndBuffer( - ref EventProviderDbContext? dbContext, + ref ProviderDbContext? dbContext, string path, List buffer, ref int count) { LogProviderDetailHeader(buffer.Select(p => p.ProviderName)); - dbContext ??= new EventProviderDbContext(path, false, Logger); + dbContext ??= new ProviderDbContext(path, false, Logger); foreach (var details in buffer) { diff --git a/src/EventLogExpert.EventDbTool/DbToolCommand.cs b/src/EventLogExpert.EventDbTool/DbToolCommand.cs index 07e3a12b..af687c74 100644 --- a/src/EventLogExpert.EventDbTool/DbToolCommand.cs +++ b/src/EventLogExpert.EventDbTool/DbToolCommand.cs @@ -54,7 +54,7 @@ protected void LogProviderDetailHeader(IEnumerable providerNames) _providerDetailFormat = "{0, -" + maxNameLength + "} {1, 8} {2, 8} {3, 8} {4, 8} {5, 8} {6, 8}"; var header = string.Format(_providerDetailFormat, "Provider Name", "Events", "Parameters", "Keywords", "Opcodes", "Tasks", "Messages"); - Logger.Info($"{header}"); + Logger.Information($"{header}"); } protected void LogProviderDetails(ProviderDetails details) @@ -69,6 +69,6 @@ protected void LogProviderDetails(ProviderDetails details) details.Tasks.Count, details.Messages.Count); - Logger.Info($"{line}"); + Logger.Information($"{line}"); } } diff --git a/src/EventLogExpert.EventDbTool/DiffDatabaseCommand.cs b/src/EventLogExpert.EventDbTool/DiffDatabaseCommand.cs index 410c3ff8..9a2a85b4 100644 --- a/src/EventLogExpert.EventDbTool/DiffDatabaseCommand.cs +++ b/src/EventLogExpert.EventDbTool/DiffDatabaseCommand.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventProviderDatabase; using EventLogExpert.Eventing.Logging; +using EventLogExpert.Eventing.ProviderDatabase; using EventLogExpert.Eventing.Providers; using Microsoft.Extensions.DependencyInjection; using System.CommandLine; @@ -69,12 +69,14 @@ internal void DiffDatabase(string firstSource, string secondSource, string newDb if (File.Exists(newDb)) { Logger.Error($"File already exists: {newDb}"); + return; } if (!string.Equals(Path.GetExtension(newDb), ".db", StringComparison.OrdinalIgnoreCase)) { Logger.Error($"New db path must have a .db extension."); + return; } @@ -87,20 +89,20 @@ internal void DiffDatabase(string firstSource, string secondSource, string newDb // Pass firstProviderNames as the skip set so providers present in the first source are // never resolved from the second source's metadata path. This is especially important when // the second source is .evtx+MTA, where each provider triggers an expensive load. - Logger.Info($"Skipping up to {firstProviderNames.Count} provider name(s) from the second source that also appear in the first source."); + Logger.Information($"Skipping up to {firstProviderNames.Count} provider name(s) from the second source that also appear in the first source."); // Defer creating the DbContext (and therefore the .db file on disk) until at least one // provider is actually about to be persisted. This prevents leaving an empty database // behind when the second source yields no new providers. - EventProviderDbContext? newDbContext = null; + ProviderDbContext? newDbContext = null; try { foreach (var details in ProviderSource.LoadProviders(secondSource, Logger, filter: null, skipProviderNames: firstProviderNames)) { - Logger.Info($"Copying {details.ProviderName} because it is present in second source but not first."); + Logger.Information($"Copying {details.ProviderName} because it is present in second source but not first."); - newDbContext ??= new EventProviderDbContext(newDb, false, Logger); + newDbContext ??= new ProviderDbContext(newDb, false, Logger); newDbContext.ProviderDetails.Add(details); @@ -109,14 +111,14 @@ internal void DiffDatabase(string firstSource, string secondSource, string newDb if (newDbContext is null) { - Logger.Warn($"No providers in the second source are missing from the first. Database was not created."); + Logger.Warning($"No providers in the second source are missing from the first. Database was not created."); return; } newDbContext.SaveChanges(); - Logger.Info($"Providers copied to new database:"); - Logger.Info($""); + Logger.Information($"Providers copied to new database:"); + Logger.Information($""); LogProviderDetailHeader(providersCopied.Select(p => p.ProviderName)); foreach (var provider in providersCopied) diff --git a/src/EventLogExpert.EventDbTool/MergeDatabaseCommand.cs b/src/EventLogExpert.EventDbTool/MergeDatabaseCommand.cs index 07b42d2b..75ba1897 100644 --- a/src/EventLogExpert.EventDbTool/MergeDatabaseCommand.cs +++ b/src/EventLogExpert.EventDbTool/MergeDatabaseCommand.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventProviderDatabase; using EventLogExpert.Eventing.Logging; +using EventLogExpert.Eventing.ProviderDatabase; using EventLogExpert.Eventing.Providers; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -75,7 +75,8 @@ internal void MergeDatabase(string source, string targetFile, bool overwriteProv if (sourceNames.Count == 0) { - Logger.Warn($"No providers were discovered in the source."); + Logger.Warning($"No providers were discovered in the source."); + return; } @@ -85,30 +86,33 @@ internal void MergeDatabase(string source, string targetFile, bool overwriteProv try { - using var probe = new EventProviderDbContext(targetFile, readOnly: false, ensureCreated: false, Logger); + using var probe = new ProviderDbContext(targetFile, readOnly: false, ensureCreated: false, Logger); targetState = probe.IsUpgradeNeeded(); } catch (DbException ex) { Logger.Error($"Failed to merge into database '{targetFile}': {ex.Message}"); + return; } if (targetState.CurrentVersion == ProviderDatabaseSchemaVersion.Unknown) { - Logger.Error($"{DatabaseSchemaMessages.UnrecognizedSchema(DatabaseSchemaMessages.TargetLabel, targetFile)}"); + Logger.Error($"{SchemaStateMessages.UnrecognizedSchema(SchemaStateMessages.TargetLabel, targetFile)}"); + return; } if (targetState.NeedsUpgrade) { Logger.Error($"Target database '{targetFile}' is at schema v{targetState.CurrentVersion} but v{ProviderDatabaseSchemaVersion.Current} is required. Run the 'upgrade' command first."); + return; } // Destructive ops beyond this point (ExecuteDelete commits immediately; SaveChanges writes) — // exceptions propagate so partial-state failures aren't masked as a benign "merge failed". - using var targetContext = new EventProviderDbContext(targetFile, false, Logger); + using var targetContext = new ProviderDbContext(targetFile, false, Logger); // Query the overlap in the database by chunking sourceNames into IN-clause batches, // rather than pulling every target ProviderName into memory. Same chunk size as the @@ -137,11 +141,11 @@ internal void MergeDatabase(string source, string targetFile, bool overwriteProv if (targetMatchingNames.Count > 0) { - Logger.Info($"The target database contains {targetMatchingNames.Count} provider row(s) matching {providerNamesInTarget.Count} provider name(s) in the source."); + Logger.Information($"The target database contains {targetMatchingNames.Count} provider row(s) matching {providerNamesInTarget.Count} provider name(s) in the source."); if (overwriteProviders) { - Logger.Info($"Removing these providers from the target database..."); + Logger.Information($"Removing these providers from the target database..."); // Chunk the IN-clause to stay below SQLite's parameter limit (default 999). Without // chunking, an --overwrite of a large overlap could throw at runtime. @@ -160,15 +164,15 @@ internal void MergeDatabase(string source, string targetFile, bool overwriteProv .ExecuteDelete(); } - Logger.Info($"Removal of {removed} provider row(s) completed."); + Logger.Information($"Removal of {removed} provider row(s) completed."); } else { - Logger.Info($"These providers will not be copied from the source."); + Logger.Information($"These providers will not be copied from the source."); } } - Logger.Info($"Copying providers from the source..."); + Logger.Information($"Copying providers from the source..."); // When not overwriting, pass the overlap as the skip set so providers that already exist // in the target are never resolved from the source's metadata path. When overwriting, no @@ -179,23 +183,23 @@ internal void MergeDatabase(string source, string targetFile, bool overwriteProv ? sourceNames.ToList() : sourceNames.Where(n => !skipForLoad.Contains(n)).ToList(); - Logger.Info($""); + Logger.Information($""); LogProviderDetailHeader(expectedCopiedNames); // Stream details into the DbContext with periodic SaveChanges. The pending batch list is // bounded by batchSize so memory stays flat regardless of source size; details are logged // AFTER each successful SaveChanges so the printed rows reflect what actually persisted // (a SaveChanges failure won't cause the log to overstate progress). - const int batchSize = 100; + const int BatchSize = 100; var copiedCount = 0; - var pendingBatch = new List(batchSize); + var pendingBatch = new List(BatchSize); foreach (var provider in ProviderSource.LoadProviders(source, Logger, filter: null, skipProviderNames: skipForLoad)) { targetContext.ProviderDetails.Add(provider); pendingBatch.Add(provider); - if (pendingBatch.Count < batchSize) { continue; } + if (pendingBatch.Count < BatchSize) { continue; } FlushBatch(targetContext, pendingBatch, ref copiedCount); } @@ -205,11 +209,11 @@ internal void MergeDatabase(string source, string targetFile, bool overwriteProv FlushBatch(targetContext, pendingBatch, ref copiedCount); } - Logger.Info($""); - Logger.Info($"Copied {copiedCount} provider(s)."); + Logger.Information($""); + Logger.Information($"Copied {copiedCount} provider(s)."); } - private void FlushBatch(EventProviderDbContext context, List batch, ref int copiedCount) + private void FlushBatch(ProviderDbContext context, List batch, ref int copiedCount) { context.SaveChanges(); diff --git a/src/EventLogExpert.EventDbTool/MtaProviderSource.cs b/src/EventLogExpert.EventDbTool/MtaProviderSource.cs index 1f2dace6..994deb91 100644 --- a/src/EventLogExpert.EventDbTool/MtaProviderSource.cs +++ b/src/EventLogExpert.EventDbTool/MtaProviderSource.cs @@ -1,6 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Providers; using EventLogExpert.Eventing.Readers; @@ -25,59 +26,6 @@ public static IReadOnlyList DiscoverProviderNames( string? filter = null) => !RegexHelper.TryCreate(filter, logger, out var regex) ? [] : DiscoverProviderNamesCore(evtxPath, logger, regex); - private static IReadOnlyList DiscoverProviderNamesCore( - string evtxPath, - ITraceLogger logger, - Regex? regex) - { - if (!File.Exists(evtxPath)) - { - logger.Error($"Evtx file not found: {evtxPath}"); - return []; - } - - var providerNames = new SortedSet(StringComparer.OrdinalIgnoreCase); - - try - { - using var reader = new EventLogReader(evtxPath, PathType.FilePath); - - if (!reader.IsValid) - { - logger.Error($"Failed to open {evtxPath} for reading. The file may be missing, corrupt, or inaccessible."); - return []; - } - - // TryGetEvents returns false both for normal end-of-results (ERROR_NO_MORE_ITEMS) and - // for read errors (corruption, access denied, etc.). Check LastErrorCode to surface - // non-terminal failures so users can distinguish "0 events" from "could not read the log". - while (reader.TryGetEvents(out var batch)) - { - foreach (var record in batch) - { - if (!string.IsNullOrEmpty(record.ProviderName)) - { - providerNames.Add(record.ProviderName); - } - } - } - - if (reader.LastErrorCode is not null) - { - logger.Warn( - $"Reading {evtxPath} may be incomplete. " + - $"EvtNext failed with Win32 error code {reader.LastErrorCode}."); - } - } - catch (Exception ex) - { - logger.Error($"Failed to read events from {evtxPath}: {ex.Message}"); - return []; - } - - return regex is null ? providerNames.ToList() : providerNames.Where(n => regex.IsMatch(n)).ToList(); - } - /// /// Returns the sibling LocaleMetaData/*.MTA files for , or an empty /// array if none are present. Logs an error in the empty case to surface the misconfiguration @@ -90,6 +38,7 @@ public static IReadOnlyList FindMtaFiles(string evtxPath, ITraceLogger l if (logDir is null) { logger.Error($"Could not determine directory for {evtxPath}."); + return []; } @@ -113,6 +62,7 @@ public static IReadOnlyList FindMtaFiles(string evtxPath, ITraceLogger l catch (Exception ex) when (ex is UnauthorizedAccessException or IOException) { logger.Error($"Cannot read LocaleMetaData folder '{localeDir}': {ex.Message}"); + return []; } @@ -121,10 +71,11 @@ public static IReadOnlyList FindMtaFiles(string evtxPath, ITraceLogger l if (mtaFiles.Length == 0) { logger.Error($"LocaleMetaData folder at {localeDir} contains no MTA files."); + return []; } - logger.Info($"Using {mtaFiles.Length} locale metadata file(s) from {localeDir}."); + logger.Information($"Using {mtaFiles.Length} locale metadata file(s) from {localeDir}."); return mtaFiles; } @@ -155,6 +106,62 @@ internal static IEnumerable LoadProviders( HashSet? seen) => LoadProvidersCore(evtxPath, logger, regex, skipProviderNames, seen); + private static IReadOnlyList DiscoverProviderNamesCore( + string evtxPath, + ITraceLogger logger, + Regex? regex) + { + if (!File.Exists(evtxPath)) + { + logger.Error($"Evtx file not found: {evtxPath}"); + + return []; + } + + var providerNames = new SortedSet(StringComparer.OrdinalIgnoreCase); + + try + { + using var reader = new EventLogReader(evtxPath, LogPathType.File); + + if (!reader.IsValid) + { + logger.Error($"Failed to open {evtxPath} for reading. The file may be missing, corrupt, or inaccessible."); + + return []; + } + + // TryGetEvents returns false both for normal end-of-results (ERROR_NO_MORE_ITEMS) and + // for read errors (corruption, access denied, etc.). Check LastErrorCode to surface + // non-terminal failures so users can distinguish "0 events" from "could not read the log". + while (reader.TryGetEvents(out var batch)) + { + foreach (var record in batch) + { + if (!string.IsNullOrEmpty(record.ProviderName)) + { + providerNames.Add(record.ProviderName); + } + } + } + + if (reader.LastErrorCode is not null) + { + logger.Warning( + $"Reading {evtxPath} may be incomplete. " + + $"EvtNext failed with Win32 error code {reader.LastErrorCode}."); + } + } + catch (Exception ex) + { + logger.Error($"Failed to read events from {evtxPath}: {ex.Message}"); + + return []; + } + + return regex is null ? providerNames.ToList() : providerNames.Where(n => regex.IsMatch(n)).ToList(); + } + private static bool IsEmpty(ProviderDetails details) => details.Events.Count == 0 && details.Keywords.Count == 0 && @@ -190,7 +197,8 @@ private static IEnumerable LoadProvidersCore( if (IsEmpty(details)) { - logger.Warn($"Skipping {providerName}: not found in any MTA file next to {evtxPath}."); + logger.Warning($"Skipping {providerName}: not found in any MTA file next to {evtxPath}."); + continue; } diff --git a/src/EventLogExpert.EventDbTool/ProviderSource.cs b/src/EventLogExpert.EventDbTool/ProviderSource.cs index 29e5c202..c076ba63 100644 --- a/src/EventLogExpert.EventDbTool/ProviderSource.cs +++ b/src/EventLogExpert.EventDbTool/ProviderSource.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventProviderDatabase; using EventLogExpert.Eventing.Logging; +using EventLogExpert.Eventing.ProviderDatabase; using EventLogExpert.Eventing.Providers; using Microsoft.EntityFrameworkCore; using System.Data.Common; @@ -125,7 +125,7 @@ public static bool ValidateSourceSchemas(string path, ITraceLogger logger) try { - using var providerContext = new EventProviderDbContext(file, readOnly: true, ensureCreated: false, logger); + using var providerContext = new ProviderDbContext(file, readOnly: true, ensureCreated: false, logger); if (!IsSourceSchemaCurrent(providerContext, file, logger)) { @@ -190,6 +190,26 @@ private static IEnumerable EnumerateSourceFiles(string path, ITraceLogge foreach (var f in evtxFiles) { yield return f; } } + private static bool IsSourceSchemaCurrent(ProviderDbContext context, string file, ITraceLogger logger) + { + var state = context.IsUpgradeNeeded(); + + if (!state.NeedsUpgrade) { return true; } + + if (state.CurrentVersion == ProviderDatabaseSchemaVersion.Unknown) + { + logger.Error( + $"{SchemaStateMessages.UnrecognizedSchema(SchemaStateMessages.SourceLabel, file)}"); + } + else + { + logger.Error( + $"Source database '{file}' is at schema v{state.CurrentVersion} but v{ProviderDatabaseSchemaVersion.Current} is required. Run the 'upgrade' command on the source first."); + } + + return false; + } + private static IEnumerable LoadDetailsFromFile( string file, ITraceLogger logger, @@ -203,7 +223,7 @@ private static IEnumerable LoadDetailsFromFile( { try { - using var context = new EventProviderDbContext(file, readOnly: true, ensureCreated: false, logger); + using var context = new ProviderDbContext(file, readOnly: true, ensureCreated: false, logger); if (!IsSourceSchemaCurrent(context, file, logger)) { return []; } @@ -249,7 +269,8 @@ private static IEnumerable LoadDetailsFromFile( } catch (Exception ex) when (ex is DbException or JsonException or InvalidDataException) { - logger.Warn($"Skipping invalid database file '{file}': {ex.Message}"); + logger.Warning($"Skipping invalid database file '{file}': {ex.Message}"); + return []; } } @@ -259,7 +280,7 @@ private static IEnumerable LoadDetailsFromFile( return MtaProviderSource.LoadProviders(file, logger, regex, skipProviderNames, seen); } - logger.Warn($"Skipping unsupported source file: {file}"); + logger.Warning($"Skipping unsupported source file: {file}"); return []; } @@ -272,14 +293,15 @@ private static IEnumerable LoadNamesFromFile(string file, ITraceLogger l { try { - using var providerContext = new EventProviderDbContext(file, readOnly: true, ensureCreated: false, logger); + using var providerContext = new ProviderDbContext(file, readOnly: true, ensureCreated: false, logger); return !IsSourceSchemaCurrent(providerContext, file, logger) ? [] : providerContext.ProviderDetails.AsNoTracking().Select(p => p.ProviderName).ToList(); } catch (DbException ex) { - logger.Warn($"Skipping invalid database file '{file}': {ex.Message}"); + logger.Warning($"Skipping invalid database file '{file}': {ex.Message}"); + return []; } } @@ -289,31 +311,11 @@ private static IEnumerable LoadNamesFromFile(string file, ITraceLogger l return MtaProviderSource.DiscoverProviderNames(file, logger); } - logger.Warn($"Skipping unsupported source file: {file}"); + logger.Warning($"Skipping unsupported source file: {file}"); return []; } - private static bool IsSourceSchemaCurrent(EventProviderDbContext context, string file, ITraceLogger logger) - { - var state = context.IsUpgradeNeeded(); - - if (!state.NeedsUpgrade) { return true; } - - if (state.CurrentVersion == ProviderDatabaseSchemaVersion.Unknown) - { - logger.Error( - $"{DatabaseSchemaMessages.UnrecognizedSchema(DatabaseSchemaMessages.SourceLabel, file)}"); - } - else - { - logger.Error( - $"Source database '{file}' is at schema v{state.CurrentVersion} but v{ProviderDatabaseSchemaVersion.Current} is required. Run the 'upgrade' command on the source first."); - } - - return false; - } - private static IEnumerable LoadProvidersIterator( string path, ITraceLogger logger, diff --git a/src/EventLogExpert.EventDbTool/ShowCommand.cs b/src/EventLogExpert.EventDbTool/ShowCommand.cs index b4783281..489bdb1d 100644 --- a/src/EventLogExpert.EventDbTool/ShowCommand.cs +++ b/src/EventLogExpert.EventDbTool/ShowCommand.cs @@ -76,7 +76,8 @@ private void ShowProviderInfo(string? source, string? filter) if (providerNames.Count == 0) { - Logger.Warn($"No providers found."); + Logger.Warning($"No providers found."); + return; } diff --git a/src/EventLogExpert.EventDbTool/UpgradeDatabaseCommand.cs b/src/EventLogExpert.EventDbTool/UpgradeDatabaseCommand.cs index 81a39b17..56d72609 100644 --- a/src/EventLogExpert.EventDbTool/UpgradeDatabaseCommand.cs +++ b/src/EventLogExpert.EventDbTool/UpgradeDatabaseCommand.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventProviderDatabase; using EventLogExpert.Eventing.Logging; +using EventLogExpert.Eventing.ProviderDatabase; using Microsoft.Extensions.DependencyInjection; using System.CommandLine; using System.Data.Common; @@ -54,7 +54,7 @@ internal void UpgradeDatabase(string file) try { - using var probe = new EventProviderDbContext(file, readOnly: false, ensureCreated: false, Logger); + using var probe = new ProviderDbContext(file, readOnly: false, ensureCreated: false, Logger); state = probe.IsUpgradeNeeded(); } catch (DbException ex) @@ -66,21 +66,21 @@ internal void UpgradeDatabase(string file) if (!state.NeedsUpgrade) { - Logger.Info($"This database does not need to be upgraded."); + Logger.Information($"This database does not need to be upgraded."); return; } if (state.CurrentVersion == ProviderDatabaseSchemaVersion.Unknown) { - Logger.Error($"{DatabaseSchemaMessages.UnrecognizedSchema(DatabaseSchemaMessages.DefaultLabel, file)}"); + Logger.Error($"{SchemaStateMessages.UnrecognizedSchema(SchemaStateMessages.DefaultLabel, file)}"); return; } if (state.CurrentVersion is 1 or 2) { - Logger.Error($"{DatabaseSchemaMessages.UnsupportedV1OrV2Schema(file, state.CurrentVersion)}"); + Logger.Error($"{SchemaStateMessages.UnsupportedV1OrV2Schema(file, state.CurrentVersion)}"); return; } @@ -88,7 +88,7 @@ internal void UpgradeDatabase(string file) // Destructive ops beyond this point — exceptions propagate so partial-state failures // aren't masked as a benign "upgrade failed". DatabaseUpgradeException only fires from // ProviderDetailsMerger before any DROP TABLE, so it's safe to catch. - using var upgradeContext = new EventProviderDbContext(file, false, Logger); + using var upgradeContext = new ProviderDbContext(file, false, Logger); try { diff --git a/src/EventLogExpert.UI/LogNameMethods.cs b/src/EventLogExpert.Eventing/Common/Channels/LogChannelMethods.cs similarity index 77% rename from src/EventLogExpert.UI/LogNameMethods.cs rename to src/EventLogExpert.Eventing/Common/Channels/LogChannelMethods.cs index 75e75c94..d05b7a0c 100644 --- a/src/EventLogExpert.UI/LogNameMethods.cs +++ b/src/EventLogExpert.Eventing/Common/Channels/LogChannelMethods.cs @@ -1,20 +1,18 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Readers; +namespace EventLogExpert.Eventing.Common.Channels; -namespace EventLogExpert.UI; - -public static class LogNameMethods +public static class LogChannelMethods { private const string MicrosoftWindowsPrefix = "Microsoft-Windows-"; - /// Live event log names hard-coded in MenuBar's File menu — filter these from dynamic log enumeration to avoid duplicates. - public static IReadOnlySet HardCodedLiveLogNames { get; } = new HashSet(StringComparer.OrdinalIgnoreCase) + /// Live event log channels hard-coded in MenuBar's File menu — filter these from dynamic log enumeration to avoid duplicates. + public static IReadOnlySet HardCodedLiveChannels { get; } = new HashSet(StringComparer.OrdinalIgnoreCase) { - LogNames.ApplicationLog, - LogNames.SystemLog, - LogNames.SecurityLog, + LogChannelNames.ApplicationLog, + LogChannelNames.SystemLog, + LogChannelNames.SecurityLog, }; public static IReadOnlyList GetMenuPath(string logName) diff --git a/src/EventLogExpert.Eventing/Readers/LogNames.cs b/src/EventLogExpert.Eventing/Common/Channels/LogChannelNames.cs similarity index 62% rename from src/EventLogExpert.Eventing/Readers/LogNames.cs rename to src/EventLogExpert.Eventing/Common/Channels/LogChannelNames.cs index 5cdd64e8..14129e96 100644 --- a/src/EventLogExpert.Eventing/Readers/LogNames.cs +++ b/src/EventLogExpert.Eventing/Common/Channels/LogChannelNames.cs @@ -1,17 +1,17 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.Eventing.Readers; +namespace EventLogExpert.Eventing.Common.Channels; -public static class LogNames +public static class LogChannelNames { public const string ApplicationLog = "Application"; public const string SecurityLog = "Security"; public const string StateLog = "State"; public const string SystemLog = "System"; - /// Live event log names that require process elevation to read. - public static IReadOnlySet AdminOnlyLiveLogNames { get; } = new HashSet(StringComparer.OrdinalIgnoreCase) + /// Live event log channels that require process elevation to read. + public static IReadOnlySet AdminOnlyLiveChannels { get; } = new HashSet(StringComparer.OrdinalIgnoreCase) { SecurityLog, StateLog, diff --git a/src/EventLogExpert.Eventing/Common/Channels/LogPathType.cs b/src/EventLogExpert.Eventing/Common/Channels/LogPathType.cs new file mode 100644 index 00000000..5f66d190 --- /dev/null +++ b/src/EventLogExpert.Eventing/Common/Channels/LogPathType.cs @@ -0,0 +1,10 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.Eventing.Common.Channels; + +public enum LogPathType +{ + Channel = 1, + File +} diff --git a/src/EventLogExpert.Eventing/Common/Databases/DatabasePathSorter.cs b/src/EventLogExpert.Eventing/Common/Databases/DatabasePathSorter.cs new file mode 100644 index 00000000..1da19cd0 --- /dev/null +++ b/src/EventLogExpert.Eventing/Common/Databases/DatabasePathSorter.cs @@ -0,0 +1,89 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.Eventing.Common.Databases; + +/// +/// Sorts provider-database identifiers (full paths or bare file names) into resolver +/// load-priority order, so that load-priority cascading works the same regardless of +/// which layer is invoking it. +/// +/// +/// +/// Order is: ascending by product name, then descending by version (numeric where the +/// version token parses as an integer so "10" sorts after "2"), then descending by raw +/// version string for tie-breaks. Inputs whose extensionless file name does not end in +/// a space-delimited version token are sorted by their full extensionless name with +/// no version key. +/// +/// +/// Returned strings are the originals — directory and extension are used only for key +/// extraction, never for reconstruction. A caller passing full paths gets full paths +/// back; a caller passing bare file names gets bare file names back. +/// +/// +public static class DatabasePathSorter +{ + /// Sort identifiers in resolver load-priority order. Returns originals unchanged. + public static IReadOnlyList Sort(IEnumerable identifiers) + { + ArgumentNullException.ThrowIfNull(identifiers); + + var source = identifiers as IReadOnlyList ?? [.. identifiers]; + + if (source.Count == 0) { return []; } + + if (source.Count == 1) { return source as string[] ?? [.. source]; } + + var keys = new SortKey[source.Count]; + + for (int i = 0; i < source.Count; i++) + { + keys[i] = SortKey.From(source[i]); + } + + Array.Sort(keys); + + var sorted = new string[source.Count]; + + for (int i = 0; i < source.Count; i++) + { + sorted[i] = keys[i].Original; + } + + return sorted; + } + + private readonly record struct SortKey(string Original, string ProductPart, int? NumericVersion, string VersionPart) + : IComparable + { + public static SortKey From(string identifier) + { + var nameSpan = Path.GetFileNameWithoutExtension(identifier.AsSpan()); + int lastSpace = nameSpan.LastIndexOf(' '); + + if (lastSpace <= 0 || lastSpace >= nameSpan.Length - 1) + { + return new SortKey(identifier, nameSpan.ToString(), null, string.Empty); + } + + var versionSpan = nameSpan[(lastSpace + 1)..]; + var productSpan = nameSpan[..lastSpace]; + int? numericVersion = int.TryParse(versionSpan, out int parsed) ? parsed : null; + + return new SortKey(identifier, productSpan.ToString(), numericVersion, versionSpan.ToString()); + } + + public int CompareTo(SortKey other) + { + int productOrder = string.CompareOrdinal(ProductPart, other.ProductPart); + + if (productOrder != 0) { return productOrder; } + + int thisNumeric = NumericVersion ?? int.MinValue; + int otherNumeric = other.NumericVersion ?? int.MinValue; + + return thisNumeric != otherNumeric ? otherNumeric.CompareTo(thisNumeric) : string.CompareOrdinal(other.VersionPart, VersionPart); + } + } +} diff --git a/src/EventLogExpert.Eventing/Common/Databases/IActiveDatabasePathsProvider.cs b/src/EventLogExpert.Eventing/Common/Databases/IActiveDatabasePathsProvider.cs new file mode 100644 index 00000000..4fc7d173 --- /dev/null +++ b/src/EventLogExpert.Eventing/Common/Databases/IActiveDatabasePathsProvider.cs @@ -0,0 +1,11 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using System.Collections.Immutable; + +namespace EventLogExpert.Eventing.Common.Databases; + +public interface IActiveDatabasePathsProvider +{ + ImmutableList ActiveDatabases { get; } +} diff --git a/src/EventLogExpert.Eventing/Models/DisplayEventModel.cs b/src/EventLogExpert.Eventing/Common/Events/DisplayEventModel.cs similarity index 88% rename from src/EventLogExpert.Eventing/Models/DisplayEventModel.cs rename to src/EventLogExpert.Eventing/Common/Events/DisplayEventModel.cs index 7e656bbe..11bf8a58 100644 --- a/src/EventLogExpert.Eventing/Models/DisplayEventModel.cs +++ b/src/EventLogExpert.Eventing/Common/Events/DisplayEventModel.cs @@ -1,14 +1,14 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Common.Channels; using System.Security.Principal; -namespace EventLogExpert.Eventing.Models; +namespace EventLogExpert.Eventing.Common.Events; public sealed record DisplayEventModel( string OwningLog /*This is the name of the log file or the live log, which we use internally*/, - PathType PathType) + LogPathType LogPathType) { public Guid? ActivityId { get; init; } @@ -45,7 +45,7 @@ public sealed record DisplayEventModel( /// Pre-rendered XML for the event. Populated by EventLogReader only when the /// log is opened with renderXml: true (currently driven by the presence of an /// applied filter that references this property). When empty, callers should use - /// to fetch the XML on demand. + /// to fetch the XML on demand. /// public string Xml { get; init; } = string.Empty; } diff --git a/src/EventLogExpert.Eventing/Models/SeverityLevel.cs b/src/EventLogExpert.Eventing/Common/Events/SeverityLevel.cs similarity index 85% rename from src/EventLogExpert.Eventing/Models/SeverityLevel.cs rename to src/EventLogExpert.Eventing/Common/Events/SeverityLevel.cs index a5a7d176..706bd6d3 100644 --- a/src/EventLogExpert.Eventing/Models/SeverityLevel.cs +++ b/src/EventLogExpert.Eventing/Common/Events/SeverityLevel.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.Eventing.Models; +namespace EventLogExpert.Eventing.Common.Events; /// Severity levels with values matching ETW standard levels (1–5). public enum SeverityLevel diff --git a/src/EventLogExpert.Eventing/EventResolvers/IDatabaseCollectionProvider.cs b/src/EventLogExpert.Eventing/EventResolvers/IDatabaseCollectionProvider.cs deleted file mode 100644 index cfb218d0..00000000 --- a/src/EventLogExpert.Eventing/EventResolvers/IDatabaseCollectionProvider.cs +++ /dev/null @@ -1,11 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -using System.Collections.Immutable; - -namespace EventLogExpert.Eventing.EventResolvers; - -public interface IDatabaseCollectionProvider -{ - ImmutableList ActiveDatabases { get; } -} diff --git a/src/EventLogExpert.Eventing/EventResolvers/IEventXmlResolver.cs b/src/EventLogExpert.Eventing/EventResolvers/IEventXmlResolver.cs deleted file mode 100644 index 9bfa0492..00000000 --- a/src/EventLogExpert.Eventing/EventResolvers/IEventXmlResolver.cs +++ /dev/null @@ -1,35 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -using EventLogExpert.Eventing.Models; - -namespace EventLogExpert.Eventing.EventResolvers; - -/// -/// Resolves the raw XML for a on demand and caches the -/// result. Implementations must be thread-safe and must coalesce concurrent requests for -/// the same event into a single underlying EvtQuery / RenderEventXml call. -/// -public interface IEventXmlResolver -{ - /// Removes all cached entries. Call when every active log is closed. - void ClearAll(); - - /// Removes cached entries for a specific log path. Call when an individual log is closed. - void ClearLog(string owningLog); - - /// - /// Returns the XML for . If is - /// already populated (because the log was opened with renderXml: true), the - /// pre-rendered value is returned immediately; otherwise the resolver re-opens the source - /// log via EvtQuery, locates the record by , - /// and renders the XML. - /// - /// The event to resolve XML for. - /// Cancellation for the caller's wait. Cancelling does not - /// invalidate or evict the in-flight resolution; concurrent callers waiting on the same - /// cache entry continue to observe the result. - /// The XML string, or if the record cannot be located - /// or rendering fails. - ValueTask GetXmlAsync(DisplayEventModel evt, CancellationToken cancellationToken = default); -} diff --git a/src/EventLogExpert.Eventing/Interop/NativeMethods.Evt.cs b/src/EventLogExpert.Eventing/Interop/NativeMethods.Evt.cs index e27b49a2..7a121ad6 100644 --- a/src/EventLogExpert.Eventing/Interop/NativeMethods.Evt.cs +++ b/src/EventLogExpert.Eventing/Interop/NativeMethods.Evt.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.Eventing.Readers; using Microsoft.Win32.SafeHandles; using System.Buffers; @@ -272,7 +272,7 @@ internal static partial EvtHandle EvtOpenChannelConfig( internal static partial EvtHandle EvtOpenLog( EvtHandle session, [MarshalAs(UnmanagedType.LPWStr)] string path, - PathType flags); + LogPathType flags); /// Gets a handle that you can use to enumerate the list of registered providers on the computer [LibraryImport(EventLogApi, SetLastError = true)] @@ -293,7 +293,7 @@ internal static partial EvtHandle EvtQuery( EvtHandle session, [MarshalAs(UnmanagedType.LPWStr)] string path, [MarshalAs(UnmanagedType.LPWStr)] string? query, - PathType flags); + LogPathType flags); /// Renders an XML fragment base on the render context that you specify [LibraryImport(EventLogApi, SetLastError = true)] diff --git a/src/EventLogExpert.Eventing/Logging/ITraceLogger.cs b/src/EventLogExpert.Eventing/Logging/ITraceLogger.cs index 24f574c7..3ba03232 100644 --- a/src/EventLogExpert.Eventing/Logging/ITraceLogger.cs +++ b/src/EventLogExpert.Eventing/Logging/ITraceLogger.cs @@ -10,15 +10,15 @@ public interface ITraceLogger { LogLevel MinimumLevel { get; } - void Trace([InterpolatedStringHandlerArgument("")] TraceLogHandler handler); + void Critical([InterpolatedStringHandlerArgument("")] CriticalLogHandler handler); void Debug([InterpolatedStringHandlerArgument("")] DebugLogHandler handler); - void Info([InterpolatedStringHandlerArgument("")] InfoLogHandler handler); + void Error([InterpolatedStringHandlerArgument("")] ErrorLogHandler handler); - void Warn([InterpolatedStringHandlerArgument("")] WarnLogHandler handler); + void Information([InterpolatedStringHandlerArgument("")] InformationLogHandler handler); - void Error([InterpolatedStringHandlerArgument("")] ErrorLogHandler handler); + void Trace([InterpolatedStringHandlerArgument("")] TraceLogHandler handler); - void Critical([InterpolatedStringHandlerArgument("")] CriticalLogHandler handler); + void Warning([InterpolatedStringHandlerArgument("")] WarningLogHandler handler); } diff --git a/src/EventLogExpert.Eventing/Logging/InfoLogHandler.cs b/src/EventLogExpert.Eventing/Logging/InformationLogHandler.cs similarity index 91% rename from src/EventLogExpert.Eventing/Logging/InfoLogHandler.cs rename to src/EventLogExpert.Eventing/Logging/InformationLogHandler.cs index de6b83bb..ecce99b6 100644 --- a/src/EventLogExpert.Eventing/Logging/InfoLogHandler.cs +++ b/src/EventLogExpert.Eventing/Logging/InformationLogHandler.cs @@ -7,13 +7,13 @@ namespace EventLogExpert.Eventing.Logging; [InterpolatedStringHandler] -public struct InfoLogHandler +public struct InformationLogHandler { private LogHandlerCore _core; public readonly bool IsEnabled => _core.IsEnabled; - public InfoLogHandler(int literalLength, int formattedCount, ITraceLogger logger, out bool isEnabled) => + public InformationLogHandler(int literalLength, int formattedCount, ITraceLogger logger, out bool isEnabled) => _core.Init(literalLength, formattedCount, logger, LogLevel.Information, out isEnabled); public void AppendLiteral(string s) => _core.AppendLiteral(s); diff --git a/src/EventLogExpert.Eventing/Logging/TraceLogger.cs b/src/EventLogExpert.Eventing/Logging/TraceLogger.cs index 9342c44f..864899d6 100644 --- a/src/EventLogExpert.Eventing/Logging/TraceLogger.cs +++ b/src/EventLogExpert.Eventing/Logging/TraceLogger.cs @@ -9,17 +9,17 @@ public class TraceLogger(LogLevel loggingLevel) : ITraceLogger { public LogLevel MinimumLevel => loggingLevel; - public void Trace(TraceLogHandler handler) => Write(handler.IsEnabled, handler.ToStringAndClear(), LogLevel.Trace); + public void Critical(CriticalLogHandler handler) => Write(handler.IsEnabled, handler.ToStringAndClear(), LogLevel.Critical); public void Debug(DebugLogHandler handler) => Write(handler.IsEnabled, handler.ToStringAndClear(), LogLevel.Debug); - public void Info(InfoLogHandler handler) => Write(handler.IsEnabled, handler.ToStringAndClear(), LogLevel.Information); + public void Error(ErrorLogHandler handler) => Write(handler.IsEnabled, handler.ToStringAndClear(), LogLevel.Error); - public void Warn(WarnLogHandler handler) => Write(handler.IsEnabled, handler.ToStringAndClear(), LogLevel.Warning); + public void Information(InformationLogHandler handler) => Write(handler.IsEnabled, handler.ToStringAndClear(), LogLevel.Information); - public void Error(ErrorLogHandler handler) => Write(handler.IsEnabled, handler.ToStringAndClear(), LogLevel.Error); + public void Trace(TraceLogHandler handler) => Write(handler.IsEnabled, handler.ToStringAndClear(), LogLevel.Trace); - public void Critical(CriticalLogHandler handler) => Write(handler.IsEnabled, handler.ToStringAndClear(), LogLevel.Critical); + public void Warning(WarningLogHandler handler) => Write(handler.IsEnabled, handler.ToStringAndClear(), LogLevel.Warning); private static void Write(bool isEnabled, string message, LogLevel level) { diff --git a/src/EventLogExpert.Eventing/Logging/WarnLogHandler.cs b/src/EventLogExpert.Eventing/Logging/WarningLogHandler.cs similarity index 91% rename from src/EventLogExpert.Eventing/Logging/WarnLogHandler.cs rename to src/EventLogExpert.Eventing/Logging/WarningLogHandler.cs index 97258d9b..e3eab8c5 100644 --- a/src/EventLogExpert.Eventing/Logging/WarnLogHandler.cs +++ b/src/EventLogExpert.Eventing/Logging/WarningLogHandler.cs @@ -7,13 +7,13 @@ namespace EventLogExpert.Eventing.Logging; [InterpolatedStringHandler] -public struct WarnLogHandler +public struct WarningLogHandler { private LogHandlerCore _core; public readonly bool IsEnabled => _core.IsEnabled; - public WarnLogHandler(int literalLength, int formattedCount, ITraceLogger logger, out bool isEnabled) => + public WarningLogHandler(int literalLength, int formattedCount, ITraceLogger logger, out bool isEnabled) => _core.Init(literalLength, formattedCount, logger, LogLevel.Warning, out isEnabled); public void AppendLiteral(string s) => _core.AppendLiteral(s); diff --git a/src/EventLogExpert.Eventing/EventProviderDatabase/CompressedJsonValueConverter.cs b/src/EventLogExpert.Eventing/ProviderDatabase/CompressedJsonValueConverter.cs similarity index 95% rename from src/EventLogExpert.Eventing/EventProviderDatabase/CompressedJsonValueConverter.cs rename to src/EventLogExpert.Eventing/ProviderDatabase/CompressedJsonValueConverter.cs index ff31f2b7..05869b44 100644 --- a/src/EventLogExpert.Eventing/EventProviderDatabase/CompressedJsonValueConverter.cs +++ b/src/EventLogExpert.Eventing/ProviderDatabase/CompressedJsonValueConverter.cs @@ -5,12 +5,21 @@ using System.IO.Compression; using System.Text.Json; -namespace EventLogExpert.Eventing.EventProviderDatabase; +namespace EventLogExpert.Eventing.ProviderDatabase; internal sealed class CompressedJsonValueConverter() : ValueConverter(v => ConvertToCompressedJson(v), v => ConvertFromCompressedJson(v)) where T : class { + public static T ConvertFromCompressedJson(byte[] value) + { + using MemoryStream memoryStream = new(value); + using GZipStream gZipStream = new(memoryStream, CompressionMode.Decompress); + + return JsonSerializer.Deserialize(gZipStream, ProviderJsonSerializerOptions.Default) + ?? throw new JsonException($"Failed to deserialize compressed JSON to type {typeof(T).Name}. The deserialized value was null."); + } + public static byte[] ConvertToCompressedJson(T value) { using MemoryStream memoryStream = new(); @@ -22,13 +31,4 @@ public static byte[] ConvertToCompressedJson(T value) return memoryStream.ToArray(); } - - public static T ConvertFromCompressedJson(byte[] value) - { - using MemoryStream memoryStream = new(value); - using GZipStream gZipStream = new(memoryStream, CompressionMode.Decompress); - - return JsonSerializer.Deserialize(gZipStream, ProviderJsonSerializerOptions.Default) - ?? throw new JsonException($"Failed to deserialize compressed JSON to type {typeof(T).Name}. The deserialized value was null."); - } } diff --git a/src/EventLogExpert.Eventing/EventProviderDatabase/DatabaseUpgradeException.cs b/src/EventLogExpert.Eventing/ProviderDatabase/DatabaseUpgradeException.cs similarity index 92% rename from src/EventLogExpert.Eventing/EventProviderDatabase/DatabaseUpgradeException.cs rename to src/EventLogExpert.Eventing/ProviderDatabase/DatabaseUpgradeException.cs index b740e6a6..2a263001 100644 --- a/src/EventLogExpert.Eventing/EventProviderDatabase/DatabaseUpgradeException.cs +++ b/src/EventLogExpert.Eventing/ProviderDatabase/DatabaseUpgradeException.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.Eventing.EventProviderDatabase; +namespace EventLogExpert.Eventing.ProviderDatabase; public sealed class DatabaseUpgradeException : Exception { diff --git a/src/EventLogExpert.Eventing/EventProviderDatabase/ProviderDatabaseSchemaState.cs b/src/EventLogExpert.Eventing/ProviderDatabase/ProviderDatabaseSchemaState.cs similarity index 81% rename from src/EventLogExpert.Eventing/EventProviderDatabase/ProviderDatabaseSchemaState.cs rename to src/EventLogExpert.Eventing/ProviderDatabase/ProviderDatabaseSchemaState.cs index 908c97d2..4443bc03 100644 --- a/src/EventLogExpert.Eventing/EventProviderDatabase/ProviderDatabaseSchemaState.cs +++ b/src/EventLogExpert.Eventing/ProviderDatabase/ProviderDatabaseSchemaState.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.Eventing.EventProviderDatabase; +namespace EventLogExpert.Eventing.ProviderDatabase; public sealed record ProviderDatabaseSchemaState(int CurrentVersion) { diff --git a/src/EventLogExpert.Eventing/EventProviderDatabase/ProviderDatabaseSchemaVersion.cs b/src/EventLogExpert.Eventing/ProviderDatabase/ProviderDatabaseSchemaVersion.cs similarity index 78% rename from src/EventLogExpert.Eventing/EventProviderDatabase/ProviderDatabaseSchemaVersion.cs rename to src/EventLogExpert.Eventing/ProviderDatabase/ProviderDatabaseSchemaVersion.cs index 6c1e14dd..9da55ca3 100644 --- a/src/EventLogExpert.Eventing/EventProviderDatabase/ProviderDatabaseSchemaVersion.cs +++ b/src/EventLogExpert.Eventing/ProviderDatabase/ProviderDatabaseSchemaVersion.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.Eventing.EventProviderDatabase; +namespace EventLogExpert.Eventing.ProviderDatabase; public static class ProviderDatabaseSchemaVersion { diff --git a/src/EventLogExpert.Eventing/EventProviderDatabase/EventProviderDbContext.cs b/src/EventLogExpert.Eventing/ProviderDatabase/ProviderDbContext.cs similarity index 91% rename from src/EventLogExpert.Eventing/EventProviderDatabase/EventProviderDbContext.cs rename to src/EventLogExpert.Eventing/ProviderDatabase/ProviderDbContext.cs index 3c6fb89f..0d4431b4 100644 --- a/src/EventLogExpert.Eventing/EventProviderDatabase/EventProviderDbContext.cs +++ b/src/EventLogExpert.Eventing/ProviderDatabase/ProviderDbContext.cs @@ -2,28 +2,27 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; -using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Providers; using Microsoft.EntityFrameworkCore; using System.Data; using System.Data.Common; -namespace EventLogExpert.Eventing.EventProviderDatabase; +namespace EventLogExpert.Eventing.ProviderDatabase; -public sealed class EventProviderDbContext : DbContext +public sealed class ProviderDbContext : DbContext { private readonly ITraceLogger? _logger; private readonly bool _readOnly; - public EventProviderDbContext(string path, bool readOnly, ITraceLogger? logger = null) + public ProviderDbContext(string path, bool readOnly, ITraceLogger? logger = null) : this(path, readOnly, true, logger) { } - public EventProviderDbContext(string path, bool readOnly, bool ensureCreated, ITraceLogger? logger = null) + public ProviderDbContext(string path, bool readOnly, bool ensureCreated, ITraceLogger? logger = null) { _logger = logger; _logger?.Debug( - $"Instantiating EventProviderDbContext. path: {path} readOnly: {readOnly} ensureCreated: {ensureCreated}"); + $"Instantiating ProviderDbContext. path: {path} readOnly: {readOnly} ensureCreated: {ensureCreated}"); Name = System.IO.Path.GetFileNameWithoutExtension(path); Path = path; @@ -148,7 +147,7 @@ public ProviderDatabaseSchemaState IsUpgradeNeeded() var state = new ProviderDatabaseSchemaState(currentVersion); _logger?.Debug( - $"{nameof(EventProviderDbContext)}.{nameof(IsUpgradeNeeded)}() for database {Path}. currentVersion: {currentVersion} needsUpgrade: {state.NeedsUpgrade}"); + $"{nameof(ProviderDbContext)}.{nameof(IsUpgradeNeeded)}() for database {Path}. currentVersion: {currentVersion} needsUpgrade: {state.NeedsUpgrade}"); return state; } @@ -168,20 +167,20 @@ public void PerformUpgradeIfNeeded() { throw new DatabaseUpgradeException( Path, - DatabaseSchemaMessages.UnrecognizedSchema(DatabaseSchemaMessages.DefaultLabel, Path)); + SchemaStateMessages.UnrecognizedSchema(SchemaStateMessages.DefaultLabel, Path)); } if (state.CurrentVersion is 1 or 2) { throw new DatabaseUpgradeException( Path, - DatabaseSchemaMessages.UnsupportedV1OrV2Schema(Path, state.CurrentVersion)); + SchemaStateMessages.UnsupportedV1OrV2Schema(Path, state.CurrentVersion)); } var size = new FileInfo(Path).Length; - _logger?.Info( - $"EventProviderDbContext upgrading database (current v{state.CurrentVersion} → v{ProviderDatabaseSchemaVersion.Current}). Size: {size} Path: {Path}"); + _logger?.Information( + $"ProviderDbContext upgrading database (current v{state.CurrentVersion} → v{ProviderDatabaseSchemaVersion.Current}). Size: {size} Path: {Path}"); var connection = Database.GetDbConnection(); Database.OpenConnection(); @@ -234,7 +233,7 @@ public void PerformUpgradeIfNeeded() size = new FileInfo(Path).Length; - _logger?.Info($"EventProviderDbContext upgrade completed. Size: {size} Path: {Path}"); + _logger?.Information($"ProviderDbContext upgrade completed. Size: {size} Path: {Path}"); } protected override void OnConfiguring(DbContextOptionsBuilder options) => diff --git a/src/EventLogExpert.Eventing/EventProviderDatabase/ProviderDetailsMerger.cs b/src/EventLogExpert.Eventing/ProviderDatabase/ProviderDetailsMerger.cs similarity index 98% rename from src/EventLogExpert.Eventing/EventProviderDatabase/ProviderDetailsMerger.cs rename to src/EventLogExpert.Eventing/ProviderDatabase/ProviderDetailsMerger.cs index ba6d3c1f..5ff1d43b 100644 --- a/src/EventLogExpert.Eventing/EventProviderDatabase/ProviderDetailsMerger.cs +++ b/src/EventLogExpert.Eventing/ProviderDatabase/ProviderDetailsMerger.cs @@ -1,10 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Providers; -namespace EventLogExpert.Eventing.EventProviderDatabase; +namespace EventLogExpert.Eventing.ProviderDatabase; internal static class ProviderDetailsMerger { @@ -39,6 +38,7 @@ public static List MergeCaseInsensitiveDuplicates( if (group.Count == 1) { merged.Add(group[0]); + continue; } @@ -80,6 +80,7 @@ private static IReadOnlyList MergeEvents( if (!seen.TryGetValue(identity, out var existing)) { seen[identity] = evt; + continue; } diff --git a/src/EventLogExpert.Eventing/EventProviderDatabase/ProviderJsonContext.cs b/src/EventLogExpert.Eventing/ProviderDatabase/ProviderJsonContext.cs similarity index 89% rename from src/EventLogExpert.Eventing/EventProviderDatabase/ProviderJsonContext.cs rename to src/EventLogExpert.Eventing/ProviderDatabase/ProviderJsonContext.cs index 93067cab..aa9d077f 100644 --- a/src/EventLogExpert.Eventing/EventProviderDatabase/ProviderJsonContext.cs +++ b/src/EventLogExpert.Eventing/ProviderDatabase/ProviderJsonContext.cs @@ -1,10 +1,10 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Providers; using System.Text.Json.Serialization; -namespace EventLogExpert.Eventing.EventProviderDatabase; +namespace EventLogExpert.Eventing.ProviderDatabase; [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(MessageModel))] diff --git a/src/EventLogExpert.Eventing/EventProviderDatabase/ProviderJsonSerializerOptions.cs b/src/EventLogExpert.Eventing/ProviderDatabase/ProviderJsonSerializerOptions.cs similarity index 90% rename from src/EventLogExpert.Eventing/EventProviderDatabase/ProviderJsonSerializerOptions.cs rename to src/EventLogExpert.Eventing/ProviderDatabase/ProviderJsonSerializerOptions.cs index c1c261a1..ec56d3fd 100644 --- a/src/EventLogExpert.Eventing/EventProviderDatabase/ProviderJsonSerializerOptions.cs +++ b/src/EventLogExpert.Eventing/ProviderDatabase/ProviderJsonSerializerOptions.cs @@ -4,7 +4,7 @@ using System.Text.Json; using System.Text.Json.Serialization.Metadata; -namespace EventLogExpert.Eventing.EventProviderDatabase; +namespace EventLogExpert.Eventing.ProviderDatabase; internal static class ProviderJsonSerializerOptions { @@ -15,6 +15,7 @@ private static JsonSerializerOptions Create() var options = new JsonSerializerOptions(); options.TypeInfoResolverChain.Add(ProviderJsonContext.Default); options.TypeInfoResolverChain.Add(new DefaultJsonTypeInfoResolver()); + return options; } } diff --git a/src/EventLogExpert.Eventing/EventProviderDatabase/DatabaseSchemaMessages.cs b/src/EventLogExpert.Eventing/ProviderDatabase/SchemaStateMessages.cs similarity index 80% rename from src/EventLogExpert.Eventing/EventProviderDatabase/DatabaseSchemaMessages.cs rename to src/EventLogExpert.Eventing/ProviderDatabase/SchemaStateMessages.cs index b6aeeb92..16078abe 100644 --- a/src/EventLogExpert.Eventing/EventProviderDatabase/DatabaseSchemaMessages.cs +++ b/src/EventLogExpert.Eventing/ProviderDatabase/SchemaStateMessages.cs @@ -1,17 +1,13 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.Eventing.EventProviderDatabase; +namespace EventLogExpert.Eventing.ProviderDatabase; -/// -/// Shared user-facing schema-state error messages. -/// -public static class DatabaseSchemaMessages +/// Shared user-facing schema-state error messages. +public static class SchemaStateMessages { public const string DefaultLabel = "Database"; - public const string SourceLabel = "Source database"; - public const string TargetLabel = "Target database"; public static string UnrecognizedSchema(string label, string path) => diff --git a/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs b/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs index 3c08cdef..632c6450 100644 --- a/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs +++ b/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs @@ -3,15 +3,14 @@ using EventLogExpert.Eventing.Interop; using EventLogExpert.Eventing.Logging; -using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Readers; using System.Runtime.InteropServices; namespace EventLogExpert.Eventing.Providers; /// -/// Represents an event provider on the local machine. EventLogExpert is a local-only tool; -/// remote-machine resolution is intentionally not supported. +/// Represents an event provider on the local machine. EventLogExpert is a local-only tool; remote-machine +/// resolution is intentionally not supported. /// public sealed class EventMessageProvider( string providerName, @@ -19,14 +18,16 @@ public sealed class EventMessageProvider( ITraceLogger? logger = null) { private static readonly HashSet s_allProviderNames = EventLogSession.GlobalSession.GetProviderNames(); - private readonly ITraceLogger? _logger = logger; + private readonly ITraceLogger? _logger = logger; private readonly IReadOnlyList? _metadataPaths = metadataPaths; private readonly string _providerName = providerName; private RegistryProvider? _registryProvider; - internal static List GetMessages( + public ProviderDetails LoadProviderDetails() => LoadProviderDetailsCore(visited: null); + + internal static List LoadMessagesFromFiles( IEnumerable legacyProviderFiles, string providerName, ITraceLogger? logger = null) @@ -124,23 +125,18 @@ internal static List GetMessages( return messages; } - public ProviderDetails LoadProviderDetails() => LoadProviderDetailsCore(visited: null); - /// - /// Loads a message-resource module using flags that honor MUI satellite resolution, with - /// fallbacks for older binaries and unresolved paths. Returns an invalid handle on failure - /// (the caller is expected to skip). + /// Loads a message-resource module using flags that honor MUI satellite resolution, with fallbacks for older + /// binaries and unresolved paths. Returns an invalid handle on failure (the caller is expected to skip). /// /// /// - /// LOAD_LIBRARY_AS_DATAFILE alone does NOT trigger MUI satellite loading. Modern - /// Windows binaries (e.g., DriverStore-installed services, in-box system EXEs/DLLs) - /// keep their RT_MESSAGETABLE resources in <binary>.mui files under - /// language subfolders rather than in the binary itself. FindResource on a - /// module loaded with only LOAD_LIBRARY_AS_DATAFILE then returns 1813 - /// (ERROR_RESOURCE_TYPE_NOT_FOUND). Combining - /// LOAD_LIBRARY_AS_IMAGE_RESOURCE with LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE - /// causes the loader to follow the MUI fallback chain — the same path + /// LOAD_LIBRARY_AS_DATAFILE alone does NOT trigger MUI satellite loading. Modern Windows binaries (e.g., + /// DriverStore-installed services, in-box system EXEs/DLLs) keep their RT_MESSAGETABLE resources in + /// <binary>.mui files under language subfolders rather than in the binary itself. FindResource + /// on a module loaded with only LOAD_LIBRARY_AS_DATAFILE then returns 1813 ( + /// ERROR_RESOURCE_TYPE_NOT_FOUND). Combining LOAD_LIBRARY_AS_IMAGE_RESOURCE with + /// LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE causes the loader to follow the MUI fallback chain — the same path /// EvtFormatMessage (and Event Viewer MMC) uses. /// /// @@ -152,11 +148,11 @@ private static LibraryHandle LoadMessageModule(string file, ITraceLogger? logger file = Environment.ExpandEnvironmentVariables(file); // Primary attempt: MUI-aware load using the path as given. Mirrors EvtFormatMessage behavior. - const LoadLibraryFlags muiAwareFlags = + const LoadLibraryFlags MuiAwareFlags = LoadLibraryFlags.LOAD_LIBRARY_AS_IMAGE_RESOURCE | LoadLibraryFlags.LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE; - var hModule = NativeMethods.LoadLibraryExW(file, IntPtr.Zero, muiAwareFlags); + var hModule = NativeMethods.LoadLibraryExW(file, IntPtr.Zero, MuiAwareFlags); int error = Marshal.GetLastWin32Error(); if (!hModule.IsInvalid) @@ -215,7 +211,7 @@ private static LibraryHandle LoadMessageModule(string file, ITraceLogger? logger logger?.Debug( $"{primaryFailureMessage} Falling back to leaf-name resolution against the system directory: {systemPath}."); - hModule = NativeMethods.LoadLibraryExW(systemPath, IntPtr.Zero, muiAwareFlags); + hModule = NativeMethods.LoadLibraryExW(systemPath, IntPtr.Zero, MuiAwareFlags); error = Marshal.GetLastWin32Error(); @@ -235,33 +231,6 @@ private static LibraryHandle LoadMessageModule(string file, ITraceLogger? logger return LibraryHandle.Zero; } - /// - /// Loads the messages for a legacy provider from the files specified in the registry. This information is stored - /// at HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\EventLog - /// - /// - private List LoadMessagesFromDlls(IEnumerable messageFilePaths) - { - _logger?.Debug($"{nameof(LoadMessagesFromDlls)} called for files {string.Join(", ", messageFilePaths)}"); - - try - { - var messages = GetMessages(messageFilePaths, _providerName, _logger); - - _logger?.Debug($"Returning {messages.Count} messages for provider {_providerName}"); - - return messages; - } - catch (Exception ex) - { - // Hide the failure. We want to allow the results from the modern provider - // to return even if we failed to load the legacy provider. - _logger?.Debug($"Failed to load legacy provider data for {_providerName}.\n{ex}"); - } - - return []; - } - /// /// Loads the messages for a modern provider. This info is stored at /// Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WINEVT @@ -350,28 +319,29 @@ private ProviderDetails LoadProviderDetailsCore(HashSet? visited) var legacyProviderFiles = _registryProvider.GetMessageFilesForLegacyProvider(_providerName); - if (legacyProviderFiles.Any()) + if (legacyProviderFiles.Any() && + TryLoadMessages(legacyProviderFiles, out var legacyMessages) && + legacyMessages.Count > 0) { - provider.Messages = LoadMessagesFromDlls(legacyProviderFiles); + provider.Messages = legacyMessages; } - else + else if (!string.IsNullOrEmpty(providerMetadata?.MessageFilePath)) { - if (string.IsNullOrEmpty(providerMetadata?.MessageFilePath)) - { - _logger?.Debug($"No message files found for provider {_providerName}. Returning empty provider details."); - } - else - { - _logger?.Debug( - $"No message files found for provider {_providerName}. Using message file from modern provider."); + _logger?.Debug( + $"No legacy messages loaded for provider {_providerName}. Using message file from modern provider."); - provider.Messages = LoadMessagesFromDlls([providerMetadata.MessageFilePath]); - } + _ = TryLoadMessages([providerMetadata.MessageFilePath], out var modernMessages); + provider.Messages = modernMessages; + } + else + { + _logger?.Debug($"No message files found for provider {_providerName}. Returning empty provider details."); } if (!string.IsNullOrEmpty(providerMetadata?.ParameterFilePath)) { - provider.Parameters = LoadMessagesFromDlls([providerMetadata.ParameterFilePath]); + _ = TryLoadMessages([providerMetadata.ParameterFilePath], out var parameterMessages); + provider.Parameters = parameterMessages; } if (provider.IsEmpty) @@ -383,11 +353,11 @@ private ProviderDetails LoadProviderDetailsCore(HashSet? visited) } /// - /// Final fallback when neither modern publisher metadata nor a legacy registry entry exists - /// for the configured provider name. Some events (notably modern channel-named providers - /// like "Microsoft-Windows-AppXDeploymentServer/Operational") carry a channel path in the - /// ProviderName slot; the real publisher must be looked up through the channel config's - /// OwningPublisher property and resolved separately. Produces no result on failure. + /// Final fallback when neither modern publisher metadata nor a legacy registry entry exists for the configured + /// provider name. Some events (notably modern channel-named providers like + /// "Microsoft-Windows-AppXDeploymentServer/Operational") carry a channel path in the ProviderName slot; the real + /// publisher must be looked up through the channel config's OwningPublisher property and resolved separately. Produces + /// no result on failure. /// private void TryFallbackToOwningPublisher(ProviderDetails target, HashSet? visited) { @@ -532,4 +502,31 @@ and not StackOverflowException } } } + + /// + /// Tries to load messages for a legacy provider from the registry-specified files + /// (HKLM\SYSTEM\CurrentControlSet\Services\EventLog). Returns false on any failure so the caller can fall back to + /// modern provider data instead of bubbling the exception. + /// + private bool TryLoadMessages(IEnumerable messageFilePaths, out List messages) + { + _logger?.Debug($"{nameof(TryLoadMessages)} called for files {string.Join(", ", messageFilePaths)}"); + + try + { + messages = LoadMessagesFromFiles(messageFilePaths, _providerName, _logger); + + _logger?.Debug($"Returning {messages.Count} messages for provider {_providerName}"); + + return true; + } + catch (Exception ex) + { + _logger?.Debug($"Failed to load legacy provider data for {_providerName}.\n{ex}"); + + messages = []; + + return false; + } + } } diff --git a/src/EventLogExpert.Eventing/Models/EventMetadata.cs b/src/EventLogExpert.Eventing/Providers/EventMetadata.cs similarity index 95% rename from src/EventLogExpert.Eventing/Models/EventMetadata.cs rename to src/EventLogExpert.Eventing/Providers/EventMetadata.cs index 9eed618c..7188065d 100644 --- a/src/EventLogExpert.Eventing/Models/EventMetadata.cs +++ b/src/EventLogExpert.Eventing/Providers/EventMetadata.cs @@ -1,9 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Providers; - -namespace EventLogExpert.Eventing.Models; +namespace EventLogExpert.Eventing.Providers; internal sealed record EventMetadata { diff --git a/src/EventLogExpert.Eventing/Models/EventModel.cs b/src/EventLogExpert.Eventing/Providers/EventModel.cs similarity index 91% rename from src/EventLogExpert.Eventing/Models/EventModel.cs rename to src/EventLogExpert.Eventing/Providers/EventModel.cs index 787a77c8..74070572 100644 --- a/src/EventLogExpert.Eventing/Models/EventModel.cs +++ b/src/EventLogExpert.Eventing/Providers/EventModel.cs @@ -1,25 +1,25 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.Eventing.Models; +namespace EventLogExpert.Eventing.Providers; public class EventModel { - public long Id { get; set; } + public string? Description { get; set; } - public byte Version { get; set; } + public long Id { get; set; } - public string? LogName { get; set; } + public required long[] Keywords { get; set; } public int Level { get; set; } + public string? LogName { get; set; } + public int Opcode { get; set; } public int Task { get; set; } - public required long[] Keywords { get; set; } - public string? Template { get; set; } - public string? Description { get; set; } + public byte Version { get; set; } } diff --git a/src/EventLogExpert.Eventing/Models/MessageModel.cs b/src/EventLogExpert.Eventing/Providers/MessageModel.cs similarity index 97% rename from src/EventLogExpert.Eventing/Models/MessageModel.cs rename to src/EventLogExpert.Eventing/Providers/MessageModel.cs index 265ae0a4..42ce86eb 100644 --- a/src/EventLogExpert.Eventing/Models/MessageModel.cs +++ b/src/EventLogExpert.Eventing/Providers/MessageModel.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.Eventing.Models; +namespace EventLogExpert.Eventing.Providers; /// /// Represents a message that was originally stored in a Message File (see diff --git a/src/EventLogExpert.Eventing/Providers/ProviderDetails.cs b/src/EventLogExpert.Eventing/Providers/ProviderDetails.cs index 234b18ef..fd6c2036 100644 --- a/src/EventLogExpert.Eventing/Providers/ProviderDetails.cs +++ b/src/EventLogExpert.Eventing/Providers/ProviderDetails.cs @@ -1,8 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; - namespace EventLogExpert.Eventing.Providers; public class ProviderDetails diff --git a/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs b/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs index 84f66062..9a496623 100644 --- a/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs +++ b/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs @@ -3,7 +3,6 @@ using EventLogExpert.Eventing.Interop; using EventLogExpert.Eventing.Logging; -using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Readers; using System.Collections.ObjectModel; using System.Runtime.InteropServices; diff --git a/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs b/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs index 7011d558..94017c69 100644 --- a/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs +++ b/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.Eventing.Logging; -using EventLogExpert.Eventing.Readers; using Microsoft.Win32; namespace EventLogExpert.Eventing.Providers; @@ -35,7 +35,7 @@ public IEnumerable GetMessageFilesForLegacyProvider(string providerName) foreach (var logSubKeyName in eventLogKey.GetSubKeyNames()) { // Skip Security and State since it requires elevation - if (LogNames.AdminOnlyLiveLogNames.Contains(logSubKeyName)) + if (LogChannelNames.AdminOnlyLiveChannels.Contains(logSubKeyName)) { continue; } @@ -93,5 +93,8 @@ public IEnumerable GetMessageFilesForLegacyProvider(string providerName) return []; } - private class OpenEventLogRegistryKeyFailedException(string msg) : Exception(msg) { /* marker exception — no extra state needed */ } + private class OpenEventLogRegistryKeyFailedException(string msg) : Exception(msg) + { + /* marker exception — no extra state needed */ + } } diff --git a/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs b/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs index 07cd7d07..a4678f29 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs @@ -1,6 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.Eventing.Interop; using System.Runtime.InteropServices; @@ -8,7 +9,7 @@ namespace EventLogExpert.Eventing.Readers; public sealed class EventLogInformation { - internal EventLogInformation(EventLogSession session, string logName, PathType pathType) + internal EventLogInformation(EventLogSession session, string logName, LogPathType pathType) { ArgumentException.ThrowIfNullOrWhiteSpace(logName); @@ -16,20 +17,20 @@ internal EventLogInformation(EventLogSession session, string logName, PathType p int error = Marshal.GetLastWin32Error(); - // Surface the real EvtOpenLog failure — without this, GetLogInfo on a NULL handle masks it as UAE. + // Surface the real EvtOpenLog failure — without this, GetLogProperty on a NULL handle masks it as UAE. if (handle.IsInvalid) { NativeMethods.ThrowEventLogException(error); } - Attributes = (int?)(uint?)GetLogInfo(handle, EvtLogPropertyId.Attributes); - CreationTime = (DateTime?)GetLogInfo(handle, EvtLogPropertyId.CreationTime); - FileSize = (long?)(ulong?)GetLogInfo(handle, EvtLogPropertyId.FileSize); - IsLogFull = (bool?)GetLogInfo(handle, EvtLogPropertyId.Full); - LastAccessTime = (DateTime?)GetLogInfo(handle, EvtLogPropertyId.LastAccessTime); - LastWriteTime = (DateTime?)GetLogInfo(handle, EvtLogPropertyId.LastWriteTime); - OldestRecordNumber = (long?)(ulong?)GetLogInfo(handle, EvtLogPropertyId.OldestRecordNumber); - RecordCount = (long?)(ulong?)GetLogInfo(handle, EvtLogPropertyId.NumberOfLogRecords); + Attributes = (int?)(uint?)GetLogProperty(handle, EvtLogPropertyId.Attributes); + CreationTime = (DateTime?)GetLogProperty(handle, EvtLogPropertyId.CreationTime); + FileSize = (long?)(ulong?)GetLogProperty(handle, EvtLogPropertyId.FileSize); + IsLogFull = (bool?)GetLogProperty(handle, EvtLogPropertyId.Full); + LastAccessTime = (DateTime?)GetLogProperty(handle, EvtLogPropertyId.LastAccessTime); + LastWriteTime = (DateTime?)GetLogProperty(handle, EvtLogPropertyId.LastWriteTime); + OldestRecordNumber = (long?)(ulong?)GetLogProperty(handle, EvtLogPropertyId.OldestRecordNumber); + RecordCount = (long?)(ulong?)GetLogProperty(handle, EvtLogPropertyId.NumberOfLogRecords); } public int? Attributes { get; } @@ -48,7 +49,7 @@ internal EventLogInformation(EventLogSession session, string logName, PathType p public long? RecordCount { get; } - private static object? GetLogInfo(EvtHandle handle, EvtLogPropertyId property) + private static object? GetLogProperty(EvtHandle handle, EvtLogPropertyId property) { IntPtr buffer = IntPtr.Zero; diff --git a/src/EventLogExpert.Eventing/Readers/EventLogReader.cs b/src/EventLogExpert.Eventing/Readers/EventLogReader.cs index 16e95443..8a954912 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogReader.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogReader.cs @@ -1,14 +1,14 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.Eventing.Interop; -using EventLogExpert.Eventing.Models; using System.Buffers; using System.Runtime.InteropServices; namespace EventLogExpert.Eventing.Readers; -public sealed partial class EventLogReader(string path, PathType pathType, bool renderXml = false) : IDisposable +public sealed partial class EventLogReader(string path, LogPathType pathType, bool renderXml = false) : IDisposable { private readonly Lock _eventLock = new(); private readonly EvtHandle _handle = @@ -101,7 +101,7 @@ public bool TryGetEvents(out EventRecord[] events, int batchSize = 30) finally { events[i].PathName = path; - events[i].PathType = pathType; + events[i].LogPathType = pathType; } } diff --git a/src/EventLogExpert.Eventing/Readers/EventLogSession.cs b/src/EventLogExpert.Eventing/Readers/EventLogSession.cs index 645448af..d51594d5 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogSession.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogSession.cs @@ -1,6 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.Eventing.Interop; using System.Runtime.InteropServices; @@ -27,7 +28,7 @@ private EventLogSession() { } internal EvtHandle UserRenderContext { get; } = CreateRenderContext(EvtRenderContextFlags.User); - public EventLogInformation GetLogInformation(string logName, PathType pathType) => new(this, logName, pathType); + public EventLogInformation GetLogInformation(string logName, LogPathType pathType) => new(this, logName, pathType); /// Gets an ordered list of all the log names on the system. public IEnumerable GetLogNames() diff --git a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs index 9f2fada1..1a0f7a4f 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.Eventing.Interop; -using EventLogExpert.Eventing.Models; using System.Collections.Concurrent; using System.Runtime.InteropServices; @@ -31,7 +31,7 @@ public EventLogWatcher(string path, string? bookmark, bool renderXml = false) _bookmark = bookmark; _renderXml = renderXml; - _queryHandle = NativeMethods.EvtQuery(EventLogSession.GlobalSession.Handle, path, null, PathType.LogName); + _queryHandle = NativeMethods.EvtQuery(EventLogSession.GlobalSession.Handle, path, null, LogPathType.Channel); int error = Marshal.GetLastWin32Error(); if (_queryHandle is { IsInvalid: false }) { return; } @@ -132,7 +132,7 @@ private void Dispose(bool disposing) } } - private void ProcessNewEvents(object? state, bool timedOut) + private void ReadAndRaiseEvents(object? state, bool timedOut) { int threadId = Environment.CurrentManagedThreadId; @@ -200,15 +200,6 @@ private void ProcessNewEvents(object? state, bool timedOut) } } - private void ThrowIfCurrentCallbackWouldDeadlock() - { - if (_callbackThreadIds.ContainsKey(Environment.CurrentManagedThreadId)) - { - throw new InvalidOperationException( - "EventLogWatcher cannot be stopped from within an EventRecordWritten handler."); - } - } - private void Subscribe() { lock (_lifecycleLock) @@ -258,7 +249,7 @@ private void Subscribe() } // Drain backlog before TP wait registration to prevent concurrent EvtNext. - ProcessNewEvents(null, false); + ReadAndRaiseEvents(null, false); lock (_lifecycleLock) { @@ -267,7 +258,16 @@ private void Subscribe() // Concurrent Unsubscribe may have torn down the subscription during the drain. if (!Volatile.Read(ref _isSubscribed)) { return; } - _waitHandle = ThreadPool.RegisterWaitForSingleObject(_newEvents, ProcessNewEvents, null, -1, false); + _waitHandle = ThreadPool.RegisterWaitForSingleObject(_newEvents, ReadAndRaiseEvents, null, -1, false); + } + } + + private void ThrowIfCurrentCallbackWouldDeadlock() + { + if (_callbackThreadIds.ContainsKey(Environment.CurrentManagedThreadId)) + { + throw new InvalidOperationException( + "EventLogWatcher cannot be stopped from within an EventRecordWritten handler."); } } diff --git a/src/EventLogExpert.Eventing/Models/EventRecord.cs b/src/EventLogExpert.Eventing/Readers/EventRecord.cs similarity index 88% rename from src/EventLogExpert.Eventing/Models/EventRecord.cs rename to src/EventLogExpert.Eventing/Readers/EventRecord.cs index f695b502..edd30aee 100644 --- a/src/EventLogExpert.Eventing/Models/EventRecord.cs +++ b/src/EventLogExpert.Eventing/Readers/EventRecord.cs @@ -1,16 +1,16 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Common.Channels; using System.Security.Principal; -namespace EventLogExpert.Eventing.Models; +namespace EventLogExpert.Eventing.Readers; public sealed record EventRecord { public string PathName { get; set; } = string.Empty; - public PathType PathType { get; set; } + public LogPathType LogPathType { get; set; } public Guid? ActivityId { get; set; } diff --git a/src/EventLogExpert.Eventing/Readers/PathType.cs b/src/EventLogExpert.Eventing/Readers/PathType.cs deleted file mode 100644 index 49621514..00000000 --- a/src/EventLogExpert.Eventing/Readers/PathType.cs +++ /dev/null @@ -1,10 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -namespace EventLogExpert.Eventing.Readers; - -public enum PathType -{ - LogName = 1, - FilePath -} diff --git a/src/EventLogExpert.Eventing/EventResolvers/EventResolver.cs b/src/EventLogExpert.Eventing/Resolvers/EventResolver.cs similarity index 74% rename from src/EventLogExpert.Eventing/EventResolvers/EventResolver.cs rename to src/EventLogExpert.Eventing/Resolvers/EventResolver.cs index 8f0fa39c..ecb2fe1f 100644 --- a/src/EventLogExpert.Eventing/EventResolvers/EventResolver.cs +++ b/src/EventLogExpert.Eventing/Resolvers/EventResolver.cs @@ -1,16 +1,17 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventProviderDatabase; +using EventLogExpert.Eventing.Common.Databases; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.Eventing.Logging; -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.ProviderDatabase; using EventLogExpert.Eventing.Providers; +using EventLogExpert.Eventing.Readers; using Microsoft.EntityFrameworkCore; using System.Collections.Concurrent; using System.Collections.Immutable; -using System.Text.RegularExpressions; -namespace EventLogExpert.Eventing.EventResolvers; +namespace EventLogExpert.Eventing.Resolvers; /// /// Unified event resolver that implements a fallback chain: @@ -18,18 +19,18 @@ namespace EventLogExpert.Eventing.EventResolvers; /// 2. SQLite provider databases (secondary) /// 3. Local provider registry (fallback) /// -public sealed partial class EventResolver : EventResolverBase, IEventResolver +public sealed class EventResolver : EventResolverBase, IEventResolver { private readonly Lock _databaseAccessLock = new(); private readonly ConcurrentDictionary _providerFromNonLocal = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary> _resolutionGates = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary _supplementalDetails = new(StringComparer.OrdinalIgnoreCase); - private ImmutableArray _dbContexts = []; + private ImmutableArray _dbContexts = []; private ImmutableArray _metadataPaths = []; public EventResolver( - IDatabaseCollectionProvider? dbCollection = null, + IActiveDatabasePathsProvider? dbCollection = null, IEventResolverCache? cache = null, ITraceLogger? logger = null) : base(cache, logger) { @@ -41,14 +42,7 @@ public EventResolver( Logger?.Debug($"{nameof(EventResolver)} was instantiated."); } - public override DisplayEventModel ResolveEvent(EventRecord eventRecord) - { - ResolveProviderDetails(eventRecord); - - return base.ResolveEvent(eventRecord); - } - - public void ResolveProviderDetails(EventRecord eventRecord) + public void LoadProviderDetails(EventRecord eventRecord) { ObjectDisposedException.ThrowIf(IsDisposed, nameof(EventResolver)); @@ -86,64 +80,20 @@ public void ResolveProviderDetails(EventRecord eventRecord) } } - public void SetMetadataPaths(IReadOnlyList metadataPaths) => _metadataPaths = [.. metadataPaths]; - - /// - /// If the database file name ends in a year or a number, such as Exchange 2019 or Windows 2016, we want to sort - /// the database files by descending version, but by ascending product name. This generally means that databases named - /// for products like Exchange will be checked for matching providers first, and Windows will be checked last, with - /// newer versions being checked before older versions. - /// - internal static IEnumerable SortDatabases(IEnumerable databasePaths) + public override DisplayEventModel ResolveEvent(EventRecord eventRecord) { - if (!databasePaths.Any()) - { - return []; - } + LoadProviderDetails(eventRecord); - var r = SplitProductAndVersionRegex(); - - return databasePaths - .Select(path => - { - var name = Path.GetFileName(path); - var directory = Path.GetDirectoryName(path); - var m = r.Match(name); - - if (m.Success) - { - var versionString = m.Groups[2].Value; - var versionWithoutExtension = Path.GetFileNameWithoutExtension(versionString); - int? numericVersion = int.TryParse(versionWithoutExtension, out var parsed) ? parsed : null; - - return new - { - Directory = directory, - FirstPart = m.Groups[1].Value + " ", - SecondPart = versionString, - NumericVersion = numericVersion - }; - } - - return new - { - Directory = directory, - FirstPart = name, - SecondPart = "", - NumericVersion = (int?)null - }; - }) - .OrderBy(n => n.FirstPart) - .ThenByDescending(n => n.NumericVersion ?? int.MinValue) - .ThenByDescending(n => n.SecondPart) - .Select(n => Path.Join(n.Directory, n.FirstPart + n.SecondPart)); + return base.ResolveEvent(eventRecord); } + public void SetMetadataPaths(IReadOnlyList metadataPaths) => _metadataPaths = [.. metadataPaths]; + protected override void Dispose(bool disposing) { if (!disposing) { return; } - ImmutableArray contextsToDispose; + ImmutableArray contextsToDispose; using (_databaseAccessLock.EnterScope()) { @@ -176,9 +126,6 @@ protected override void Dispose(bool disposing) }); } - [GeneratedRegex("^(.+) (\\S+)$")] - private static partial Regex SplitProductAndVersionRegex(); - private void LoadDatabases(IEnumerable databasePaths) { Logger?.Debug($"{nameof(LoadDatabases)} was called with {databasePaths.Count()} {nameof(databasePaths)}."); @@ -195,10 +142,10 @@ private void LoadDatabases(IEnumerable databasePaths) _dbContexts = []; - var databasesToLoad = SortDatabases(databasePaths); + var databasesToLoad = DatabasePathSorter.Sort(databasePaths); var unknownDbs = new List(); var obsoleteDbs = new List(); - var newContexts = new List(); + var newContexts = new List(); foreach (var file in databasesToLoad) { @@ -207,7 +154,7 @@ private void LoadDatabases(IEnumerable databasePaths) throw new FileNotFoundException(file); } - var providerDb = new EventProviderDbContext(file, readOnly: true, ensureCreated: false, Logger); + var providerDb = new ProviderDbContext(file, readOnly: true, ensureCreated: false, Logger); var state = providerDb.IsUpgradeNeeded(); if (state.CurrentVersion == ProviderDatabaseSchemaVersion.Unknown) diff --git a/src/EventLogExpert.Eventing/EventResolvers/EventResolverBase.cs b/src/EventLogExpert.Eventing/Resolvers/EventResolverBase.cs similarity index 99% rename from src/EventLogExpert.Eventing/EventResolvers/EventResolverBase.cs rename to src/EventLogExpert.Eventing/Resolvers/EventResolverBase.cs index b451b2ca..d7ab44f3 100644 --- a/src/EventLogExpert.Eventing/EventResolvers/EventResolverBase.cs +++ b/src/EventLogExpert.Eventing/Resolvers/EventResolverBase.cs @@ -1,9 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.Eventing.Interop; using EventLogExpert.Eventing.Logging; -using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Providers; using EventLogExpert.Eventing.Readers; using System.Buffers; @@ -12,7 +12,7 @@ using System.Text; using System.Text.RegularExpressions; -namespace EventLogExpert.Eventing.EventResolvers; +namespace EventLogExpert.Eventing.Resolvers; public partial class EventResolverBase : IDisposable { @@ -474,14 +474,14 @@ private DisplayEventModel CreateEventModel( ProviderDetails? descriptionDetails, ProviderDetails? supplemental, EventModel? supplementalModernEvent) => - new(eventRecord.PathName, eventRecord.PathType) + new(eventRecord.PathName, eventRecord.LogPathType) { ActivityId = eventRecord.ActivityId, ComputerName = _cache?.GetOrAddValue(eventRecord.ComputerName) ?? eventRecord.ComputerName, Description = ResolveDescription(eventRecord, details, descriptionDetails, modernEvent, supplemental, supplementalModernEvent), Id = eventRecord.Id, Keywords = GetKeywordsFromBitmask(eventRecord, details, supplemental), - Level = Severity.GetString(eventRecord.Level), + Level = SeverityFormatter.Format(eventRecord.Level), LogName = _cache?.GetOrAddValue(eventRecord.LogName) ?? eventRecord.LogName, ProcessId = eventRecord.ProcessId, RecordId = eventRecord.RecordId, @@ -692,7 +692,7 @@ private string FormatDescription( } catch (InvalidOperationException ex) { - Logger?.Warn($"{nameof(FormatDescription)}: InvalidOperationException - PropertyCount={properties.Count}, Template={descriptionTemplate}, Exception={ex.Message}"); + Logger?.Warning($"{nameof(FormatDescription)}: InvalidOperationException - PropertyCount={properties.Count}, Template={descriptionTemplate}, Exception={ex.Message}"); // If the regex fails to match, then we just return the original description. returnDescription = description.ToString(); @@ -701,7 +701,7 @@ private string FormatDescription( } catch (Exception ex) { - Logger?.Warn($"{nameof(FormatDescription)}: Unexpected exception - PropertyCount={properties.Count}, Template={descriptionTemplate}, Exception={ex}"); + Logger?.Warning($"{nameof(FormatDescription)}: Unexpected exception - PropertyCount={properties.Count}, Template={descriptionTemplate}, Exception={ex}"); return DefaultFailedDescription; } diff --git a/src/EventLogExpert.Eventing/EventResolvers/EventResolverCache.cs b/src/EventLogExpert.Eventing/Resolvers/EventResolverCache.cs similarity index 95% rename from src/EventLogExpert.Eventing/EventResolvers/EventResolverCache.cs rename to src/EventLogExpert.Eventing/Resolvers/EventResolverCache.cs index 6bfec3a8..b8acaa11 100644 --- a/src/EventLogExpert.Eventing/EventResolvers/EventResolverCache.cs +++ b/src/EventLogExpert.Eventing/Resolvers/EventResolverCache.cs @@ -3,7 +3,7 @@ using System.Collections.Concurrent; -namespace EventLogExpert.Eventing.EventResolvers; +namespace EventLogExpert.Eventing.Resolvers; public sealed class EventResolverCache : IEventResolverCache { diff --git a/src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs b/src/EventLogExpert.Eventing/Resolvers/EventXmlResolver.cs similarity index 85% rename from src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs rename to src/EventLogExpert.Eventing/Resolvers/EventXmlResolver.cs index 264ba890..9571aa62 100644 --- a/src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs +++ b/src/EventLogExpert.Eventing/Resolvers/EventXmlResolver.cs @@ -1,17 +1,17 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Eventing.Common.Channels; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.Eventing.Interop; -using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Readers; -namespace EventLogExpert.Eventing.EventResolvers; +namespace EventLogExpert.Eventing.Resolvers; /// -/// Bounded LRU cache that resolves and caches event XML on demand. Concurrent requests -/// for the same key are coalesced through a shared task so the -/// underlying EvtQuery / RenderEventXml P/Invoke pair runs at most once -/// per cache entry. Eviction is silent: a request for an evicted key triggers a fresh +/// Bounded LRU cache that resolves and caches event XML on demand. Concurrent requests for the same key are +/// coalesced through a shared task so the underlying EvtQuery / RenderEventXml +/// P/Invoke pair runs at most once per cache entry. Eviction is silent: a request for an evicted key triggers a fresh /// resolve and re-cache. /// public sealed class EventXmlResolver : IEventXmlResolver @@ -19,23 +19,23 @@ public sealed class EventXmlResolver : IEventXmlResolver private const int DefaultInitialCapacity = 256; private const int DefaultMaxCapacity = 4096; - private static readonly Func s_defaultResolveStrategyFunc = DefaultResolveStrategy; + private static readonly Func s_defaultResolveStrategyFunc = DefaultResolveStrategy; private readonly Dictionary> _cache; private readonly Lock _cacheLock = new(); private readonly LinkedList _lruOrder = []; private readonly int _maxCapacity; - private readonly Func _resolveStrategy; + private readonly Func _resolveStrategy; private int _capacity; public EventXmlResolver() : this(s_defaultResolveStrategyFunc) { } - internal EventXmlResolver(Func resolveStrategy) + internal EventXmlResolver(Func resolveStrategy) : this(resolveStrategy, DefaultInitialCapacity, DefaultMaxCapacity) { } internal EventXmlResolver( - Func resolveStrategy, + Func resolveStrategy, int initialCapacity, int maxCapacity) { @@ -58,7 +58,7 @@ public void ClearAll() } } - public void ClearLog(string owningLog) + public void ClearXmlCacheForLog(string owningLog) { if (string.IsNullOrEmpty(owningLog)) { return; } @@ -96,7 +96,7 @@ public ValueTask GetXmlAsync(DisplayEventModel evt, CancellationToken ca return new ValueTask(string.Empty); } - var key = new XmlCacheKey(evt.OwningLog, evt.RecordId.Value, evt.PathType); + var key = new XmlCacheKey(evt.OwningLog, evt.RecordId.Value, evt.LogPathType); Lazy> lazy = GetOrCreateEntry(key); return AwaitLazyAsync(lazy, cancellationToken); @@ -115,7 +115,7 @@ private static async ValueTask AwaitLazyAsync(Lazy> lazy, C } } - private static string DefaultResolveStrategy(string owningLog, long recordId, PathType pathType) + private static string DefaultResolveStrategy(string owningLog, long recordId, LogPathType pathType) { using EvtHandle handle = NativeMethods.EvtQuery( EventLogSession.GlobalSession.Handle, @@ -201,7 +201,7 @@ private async Task ResolveAndEvictOnFailureAsync(XmlCacheKey key) { try { - return await Task.Run(() => _resolveStrategy(key.OwningLog, key.RecordId, key.PathType)).ConfigureAwait(false); + return await Task.Run(() => _resolveStrategy(key.OwningLog, key.RecordId, key.LogPathType)).ConfigureAwait(false); } catch { @@ -211,7 +211,7 @@ private async Task ResolveAndEvictOnFailureAsync(XmlCacheKey key) } } - private readonly record struct XmlCacheKey(string OwningLog, long RecordId, PathType PathType); + private readonly record struct XmlCacheKey(string OwningLog, long RecordId, LogPathType LogPathType); private sealed record CacheEntry(XmlCacheKey Key, Lazy> Lazy); } diff --git a/src/EventLogExpert.Eventing/EventResolvers/IEventResolver.cs b/src/EventLogExpert.Eventing/Resolvers/IEventResolver.cs similarity index 65% rename from src/EventLogExpert.Eventing/EventResolvers/IEventResolver.cs rename to src/EventLogExpert.Eventing/Resolvers/IEventResolver.cs index 3d5b7e29..32370efc 100644 --- a/src/EventLogExpert.Eventing/EventResolvers/IEventResolver.cs +++ b/src/EventLogExpert.Eventing/Resolvers/IEventResolver.cs @@ -1,16 +1,17 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Common.Events; +using EventLogExpert.Eventing.Readers; -namespace EventLogExpert.Eventing.EventResolvers; +namespace EventLogExpert.Eventing.Resolvers; /// Resolves event details from an . public interface IEventResolver : IDisposable { - DisplayEventModel ResolveEvent(EventRecord eventRecord); + void LoadProviderDetails(EventRecord eventRecord); - void ResolveProviderDetails(EventRecord eventRecord); + DisplayEventModel ResolveEvent(EventRecord eventRecord); void SetMetadataPaths(IReadOnlyList metadataPaths); } diff --git a/src/EventLogExpert.Eventing/EventResolvers/IEventResolverCache.cs b/src/EventLogExpert.Eventing/Resolvers/IEventResolverCache.cs similarity index 82% rename from src/EventLogExpert.Eventing/EventResolvers/IEventResolverCache.cs rename to src/EventLogExpert.Eventing/Resolvers/IEventResolverCache.cs index ebf4e03d..c608d691 100644 --- a/src/EventLogExpert.Eventing/EventResolvers/IEventResolverCache.cs +++ b/src/EventLogExpert.Eventing/Resolvers/IEventResolverCache.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.Eventing.EventResolvers; +namespace EventLogExpert.Eventing.Resolvers; public interface IEventResolverCache { diff --git a/src/EventLogExpert.Eventing/Resolvers/IEventXmlResolver.cs b/src/EventLogExpert.Eventing/Resolvers/IEventXmlResolver.cs new file mode 100644 index 00000000..490ea3ec --- /dev/null +++ b/src/EventLogExpert.Eventing/Resolvers/IEventXmlResolver.cs @@ -0,0 +1,34 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; + +namespace EventLogExpert.Eventing.Resolvers; + +/// +/// Resolves the raw XML for a on demand and caches the result. Implementations +/// must be thread-safe and must coalesce concurrent requests for the same event into a single underlying +/// EvtQuery / RenderEventXml call. +/// +public interface IEventXmlResolver +{ + /// Removes all cached entries. Call when every active log is closed. + void ClearAll(); + + /// Removes cached entries for a specific log path. Call when an individual log is closed. + void ClearXmlCacheForLog(string owningLog); + + /// + /// Returns the XML for . If is already populated + /// (because the log was opened with renderXml: true), the pre-rendered value is returned immediately; otherwise + /// the resolver re-opens the source log via EvtQuery, locates the record by + /// , and renders the XML. + /// + /// The event to resolve XML for. + /// + /// Cancellation for the caller's wait. Cancelling does not invalidate or evict the + /// in-flight resolution; concurrent callers waiting on the same cache entry continue to observe the result. + /// + /// The XML string, or if the record cannot be located or rendering fails. + ValueTask GetXmlAsync(DisplayEventModel evt, CancellationToken cancellationToken = default); +} diff --git a/src/EventLogExpert.Eventing/Models/Severity.cs b/src/EventLogExpert.Eventing/Resolvers/SeverityFormatter.cs similarity index 61% rename from src/EventLogExpert.Eventing/Models/Severity.cs rename to src/EventLogExpert.Eventing/Resolvers/SeverityFormatter.cs index 303a6975..b92351c3 100644 --- a/src/EventLogExpert.Eventing/Models/Severity.cs +++ b/src/EventLogExpert.Eventing/Resolvers/SeverityFormatter.cs @@ -1,16 +1,18 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Common.Events; -public static class Severity +namespace EventLogExpert.Eventing.Resolvers; + +public static class SeverityFormatter { /// Maps an ETW level byte to its display string. /// - /// ETW level 0 is technically "LogAlways", but the Windows Event Viewer MMC and - /// wevtutil both render it as "Information". We match that behavior intentionally. + /// ETW level 0 is technically "LogAlways", but the Windows Event Viewer MMC and wevtutil both render it as + /// "Information". We match that behavior intentionally. /// - public static string GetString(byte? level) => level switch + public static string Format(byte? level) => level switch { 0 => nameof(SeverityLevel.Information), // LogAlways — rendered as Information by Windows 1 => nameof(SeverityLevel.Critical), diff --git a/src/EventLogExpert.UI/FilterMethods.cs b/src/EventLogExpert.UI/FilterMethods.cs index 2fd048ca..3ef39384 100644 --- a/src/EventLogExpert.UI/FilterMethods.cs +++ b/src/EventLogExpert.UI/FilterMethods.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.UI.Models; namespace EventLogExpert.UI; @@ -14,7 +14,6 @@ public static class FilterMethods (a, b) => WithTieBreaker(a.TimeCreated.CompareTo(b.TimeCreated), a, b); private static readonly Comparison s_ascByActivityId = (a, b) => WithTieBreaker(Nullable.Compare(a.ActivityId, b.ActivityId), a, b); - private static readonly Comparison s_ascByLog = (a, b) => WithTieBreaker(string.Compare(a.LogName, b.LogName, StringComparison.Ordinal), a, b); private static readonly Comparison s_ascByComputerName = diff --git a/src/EventLogExpert.UI/Interfaces/IFilterService.cs b/src/EventLogExpert.UI/Interfaces/IFilterService.cs index af3ee324..fcb6b4af 100644 --- a/src/EventLogExpert.UI/Interfaces/IFilterService.cs +++ b/src/EventLogExpert.UI/Interfaces/IFilterService.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.UI.Models; namespace EventLogExpert.UI.Interfaces; diff --git a/src/EventLogExpert.UI/Models/CompiledFilter.cs b/src/EventLogExpert.UI/Models/CompiledFilter.cs index c0fcfeab..b2d4a80c 100644 --- a/src/EventLogExpert.UI/Models/CompiledFilter.cs +++ b/src/EventLogExpert.UI/Models/CompiledFilter.cs @@ -1,16 +1,14 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Common.Events; namespace EventLogExpert.UI.Models; /// -/// Immutable compiled artifact: the runnable predicate plus the cached XML-access flag -/// used to decide whether logs must be opened with pre-rendered XML. +/// Immutable compiled artifact: the runnable predicate plus the cached XML-access flag used to decide whether +/// logs must be opened with pre-rendered XML. /// /// The compiled lambda evaluated against each . -/// -/// true when the source expression references . -/// +/// true when the source expression references . public sealed record CompiledFilter(Func Predicate, bool RequiresXml); diff --git a/src/EventLogExpert.UI/Models/EventFilter.cs b/src/EventLogExpert.UI/Models/EventFilter.cs index 3edd6a3f..1dedb3d2 100644 --- a/src/EventLogExpert.UI/Models/EventFilter.cs +++ b/src/EventLogExpert.UI/Models/EventFilter.cs @@ -25,7 +25,7 @@ public EventFilter(FilterDateModel? dateFilter, ImmutableList filte /// Construction-time snapshot used by . public ImmutableArray Snapshots { get; } - /// True when any filter or sub-filter references . + /// True when any filter or sub-filter references . public bool RequiresXml { get; } private static bool ComputeRequiresXml(ImmutableList filters) diff --git a/src/EventLogExpert.UI/Models/EventLogData.cs b/src/EventLogExpert.UI/Models/EventLogData.cs index f6f90ecf..21f7e60c 100644 --- a/src/EventLogExpert.UI/Models/EventLogData.cs +++ b/src/EventLogExpert.UI/Models/EventLogData.cs @@ -1,14 +1,14 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; -using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Common.Channels; +using EventLogExpert.Eventing.Common.Events; namespace EventLogExpert.UI.Models; public sealed record EventLogData( string Name, - PathType Type, + LogPathType Type, IReadOnlyList Events) { public EventLogId Id { get; } = EventLogId.Create(); diff --git a/src/EventLogExpert.UI/Models/EventTableModel.cs b/src/EventLogExpert.UI/Models/EventTableModel.cs index 4e097801..b5e605c2 100644 --- a/src/EventLogExpert.UI/Models/EventTableModel.cs +++ b/src/EventLogExpert.UI/Models/EventTableModel.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Common.Channels; namespace EventLogExpert.UI.Models; @@ -13,7 +13,7 @@ public sealed record EventTableModel(EventLogId Id) public string LogName { get; init; } = string.Empty; - public PathType PathType { get; init; } + public LogPathType LogPathType { get; init; } public bool IsCombined { get; init; } diff --git a/src/EventLogExpert.UI/Services/ApplicationRestartService.cs b/src/EventLogExpert.UI/Services/ApplicationRestartService.cs index 2cb55a41..a1dc942a 100644 --- a/src/EventLogExpert.UI/Services/ApplicationRestartService.cs +++ b/src/EventLogExpert.UI/Services/ApplicationRestartService.cs @@ -26,7 +26,7 @@ public async Task TryRestartAsync(string launchArguments = "") if (reason == AppRestartFailureReason.RestartPending) { - _traceLogger.Info($"{nameof(ApplicationRestartService)}.{nameof(TryRestartAsync)}: restart already pending"); + _traceLogger.Information($"{nameof(ApplicationRestartService)}.{nameof(TryRestartAsync)}: restart already pending"); return true; } diff --git a/src/EventLogExpert.UI/Services/BannerService.cs b/src/EventLogExpert.UI/Services/BannerService.cs index 3ee4024d..829f41d8 100644 --- a/src/EventLogExpert.UI/Services/BannerService.cs +++ b/src/EventLogExpert.UI/Services/BannerService.cs @@ -398,7 +398,7 @@ private void RaiseStateChanged() } catch (Exception ex) { - SafeLog(() => _traceLogger.Warn( + SafeLog(() => _traceLogger.Warning( $"{nameof(BannerService)}.{nameof(StateChanged)}: subscriber threw: {ex}")); } } diff --git a/src/EventLogExpert.UI/Services/DatabaseService.cs b/src/EventLogExpert.UI/Services/DatabaseService.cs index bddb8e92..f017cf4b 100644 --- a/src/EventLogExpert.UI/Services/DatabaseService.cs +++ b/src/EventLogExpert.UI/Services/DatabaseService.cs @@ -1,21 +1,20 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventProviderDatabase; -using EventLogExpert.Eventing.EventResolvers; +using EventLogExpert.Eventing.Common.Databases; using EventLogExpert.Eventing.Logging; +using EventLogExpert.Eventing.ProviderDatabase; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Options; using Microsoft.Data.Sqlite; using System.Collections.Immutable; using System.IO.Compression; -using System.Text.RegularExpressions; using System.Threading.Channels; namespace EventLogExpert.UI.Services; -public sealed partial class DatabaseService : IDatabaseService, IDatabaseCollectionProvider +public sealed class DatabaseService : IDatabaseService, IActiveDatabasePathsProvider { public const string UpgradeBackupSuffix = ".upgrade.bak"; @@ -99,7 +98,7 @@ public async Task ClassifyEntriesAsync(CancellationToken cancellationToken = def continue; } - using var context = new EventProviderDbContext( + using var context = new ProviderDbContext( entry.FullPath, readOnly: false, ensureCreated: false, @@ -115,7 +114,7 @@ public async Task ClassifyEntriesAsync(CancellationToken cancellationToken = def { perFile[entry.FileName] = (DatabaseStatus.ClassificationFailed, false); - SafeLog(() => _traceLogger.Warn( + SafeLog(() => _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(ClassifyEntriesAsync)} failed to classify '{entry.FileName}': {ex}")); } } @@ -200,7 +199,7 @@ public async ValueTask DisposeAsync() } catch (Exception ex) { - SafeLog(() => _traceLogger.Warn( + SafeLog(() => _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(DisposeAsync)}: cancellation source faulted: {ex}")); } @@ -214,7 +213,7 @@ public async ValueTask DisposeAsync() } catch (Exception ex) { - SafeLog(() => _traceLogger.Warn( + SafeLog(() => _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(DisposeAsync)}: consumer task faulted: {ex}")); } @@ -235,7 +234,7 @@ public async Task> EnumerateZipDbEntryNamesAsync( } catch (Exception ex) when (ex is not OperationCanceledException) { - _traceLogger.Warn( + _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(EnumerateZipDbEntryNamesAsync)} failed to open '{sourceZipPath}': {ex}"); return []; @@ -341,7 +340,7 @@ public async Task ImportAsync( { failures.Add(new ImportFailure(fileName, ex.Message)); - _traceLogger.Warn( + _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(ImportAsync)} failed to copy '{fileName}': {ex}"); } } @@ -381,7 +380,7 @@ public async Task ImportAsync( } catch (Exception ex) when (ex is not OperationCanceledException) { - _traceLogger.Warn( + _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(ImportAsync)} post-import classification failed: {ex}"); } @@ -446,7 +445,7 @@ public void Refresh() StringComparer.OrdinalIgnoreCase); var fileNames = EnumerateDatabaseFileNames(); - var sortedFileNames = SortDatabases(fileNames); + var sortedFileNames = DatabasePathSorter.Sort(fileNames); ImmutableList nextSnapshot = sortedFileNames .Select(fileName => @@ -535,7 +534,7 @@ public async Task RemoveAsync( } catch (Exception ex) { - SafeLog(() => _traceLogger.Warn( + SafeLog(() => _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(RemoveAsync)}: prepare-for-deletion callback failed: {ex}")); RestoreEnabledIfChanged(fileName, wasEnabled); @@ -556,7 +555,7 @@ public async Task RemoveAsync( } catch (Exception ex) { - SafeLog(() => _traceLogger.Warn( + SafeLog(() => _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(RemoveAsync)}: deleting files for '{fileName}' failed: {ex}")); RestoreEnabledIfChanged(fileName, wasEnabled); @@ -601,7 +600,7 @@ public async Task RestoreFromBackupAsync(string fileName, CancellationToke } catch (Exception ex) { - SafeLog(() => _traceLogger.Warn( + SafeLog(() => _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(RestoreFromBackupAsync)} post-restore classification failed: {ex}")); } @@ -785,52 +784,6 @@ private static void SafeLog(Action log) catch { /* Logger faults must not propagate from defensive logging sites. */ } } - private static IEnumerable SortDatabases(IEnumerable databases) - { - if (!databases.Any()) { return []; } - - var splitter = SplitFileName(); - - return databases - .Select(name => - { - var nameWithoutExt = Path.GetFileNameWithoutExtension(name); - var match = splitter.Match(nameWithoutExt); - - if (match.Success) - { - var versionString = match.Groups[2].Value; - - // Try to parse the version as a number for proper numeric ordering. - // This ensures "10" sorts after "2" rather than before it (lexicographic). - int? numericVersion = int.TryParse(versionString, out var parsed) ? parsed : null; - - return new - { - OriginalName = name, - FirstPart = match.Groups[1].Value + " ", - SecondPart = versionString, - NumericVersion = numericVersion - }; - } - - return new - { - OriginalName = name, - FirstPart = nameWithoutExt, - SecondPart = "", - NumericVersion = (int?)null - }; - }) - .OrderBy(name => name.FirstPart) - .ThenByDescending(name => name.NumericVersion ?? int.MinValue) - .ThenByDescending(name => name.SecondPart) - .Select(name => name.OriginalName); - } - - [GeneratedRegex("^(.+) (\\S+)$")] - private static partial Regex SplitFileName(); - private static void WalCheckpoint(string dbPath) { using (var connection = new SqliteConnection($"Data Source={dbPath}")) @@ -861,7 +814,7 @@ private async Task ConsumeUpgradeQueueAsync() } catch (Exception ex) { - SafeLog(() => _traceLogger.Warn( + SafeLog(() => _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(ConsumeUpgradeQueueAsync)}: consumer faulted: {ex}")); } finally @@ -927,7 +880,7 @@ private IEnumerable EnumerateDatabaseFileNames() } catch (Exception ex) { - _traceLogger.Warn($"{nameof(DatabaseService)}.{nameof(EnumerateDatabaseFileNames)} failed: {ex}"); + _traceLogger.Warning($"{nameof(DatabaseService)}.{nameof(EnumerateDatabaseFileNames)} failed: {ex}"); return []; } } @@ -953,7 +906,7 @@ private IEnumerable EnumerateDatabaseFileNames() { failures.Add(new ImportFailure(zipFileName, $"Could not open archive: {ex.Message}")); - _traceLogger.Warn( + _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(ImportZipAsync)} failed to open '{zipFileName}': {ex}"); return (imported, failures, importedNames); @@ -993,7 +946,7 @@ private IEnumerable EnumerateDatabaseFileNames() { failures.Add(new ImportFailure(entry.Name, ex.Message)); - _traceLogger.Warn( + _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(ImportZipAsync)} failed to extract '{entry.Name}' from '{zipFileName}': {ex}"); try @@ -1005,7 +958,7 @@ private IEnumerable EnumerateDatabaseFileNames() } catch (Exception cleanupEx) { - _traceLogger.Warn( + _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(ImportZipAsync)} failed to clean up partial extract '{destinationPath}': {cleanupEx}"); } } @@ -1049,7 +1002,7 @@ private bool ProbeOrCleanupBackup(DatabaseEntry entry, DatabaseStatus status) } catch (Exception ex) when (ex is not OperationCanceledException) { - SafeLog(() => _traceLogger.Warn( + SafeLog(() => _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(ProbeOrCleanupBackup)} probe failed for '{entry.FileName}': {ex}")); return true; @@ -1064,7 +1017,7 @@ private bool ProbeOrCleanupBackup(DatabaseEntry entry, DatabaseStatus status) } catch (Exception ex) when (ex is not OperationCanceledException) { - SafeLog(() => _traceLogger.Warn( + SafeLog(() => _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(ProbeOrCleanupBackup)} stale .upgrade.bak cleanup failed for '{entry.FileName}': {ex}")); } } @@ -1146,7 +1099,7 @@ private async Task ProcessBatchAsync(UpgradeBatch batch) var message = ex is DatabaseUpgradeException dbEx ? dbEx.Reason : ex.Message; failed.Add(new UpgradeFailure(entry.FileName, message)); - SafeLog(() => _traceLogger.Warn( + SafeLog(() => _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(ProcessBatchAsync)}: '{entry.FileName}' upgrade threw: {ex}")); } } @@ -1162,7 +1115,7 @@ private async Task ProcessBatchAsync(UpgradeBatch batch) } catch (Exception ex) { - SafeLog(() => _traceLogger.Warn( + SafeLog(() => _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(ProcessBatchAsync)}: unhandled batch error: {ex}")); batch.Tcs.TrySetException(ex); @@ -1188,7 +1141,7 @@ private void RaiseEntriesChanged() } catch (Exception ex) { - SafeLog(() => _traceLogger.Warn( + SafeLog(() => _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(RaiseEntriesChanged)}: subscriber threw: {ex}")); } } @@ -1248,7 +1201,7 @@ private bool RestoreFilesCore(DatabaseEntry entry) if (!File.Exists(backupPath)) { - SafeLog(() => _traceLogger.Warn( + SafeLog(() => _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(RestoreFromBackupAsync)}: '{backupPath}' missing; nothing to restore.")); return false; @@ -1266,7 +1219,7 @@ private bool RestoreFilesCore(DatabaseEntry entry) } catch (Exception ex) when (ex is not OperationCanceledException) { - SafeLog(() => _traceLogger.Warn( + SafeLog(() => _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(RestoreFromBackupAsync)}: copy from '{backupPath}' to '{mainPath}' failed: {ex}")); return false; @@ -1288,7 +1241,7 @@ private void SafeRaise(EventHandler? handler, TArgs args, string e } catch (Exception ex) { - SafeLog(() => _traceLogger.Warn( + SafeLog(() => _traceLogger.Warning( $"{nameof(DatabaseService)}.{eventName}: subscriber threw: {ex}")); } } @@ -1302,7 +1255,7 @@ private async Task StartInitialClassificationAsync() } catch (Exception ex) { - SafeLog(() => _traceLogger.Warn( + SafeLog(() => _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(StartInitialClassificationAsync)}: initial classification failed: {ex}")); } } @@ -1318,7 +1271,7 @@ private bool TryDeleteFile(string path, string callerName) catch (Exception ex) when (ex is not OperationCanceledException) { SafeLog(() => - _traceLogger.Warn($"{nameof(DatabaseService)}.{callerName}: delete failed for '{path}': {ex}")); + _traceLogger.Warning($"{nameof(DatabaseService)}.{callerName}: delete failed for '{path}': {ex}")); return false; } @@ -1417,7 +1370,7 @@ await Task.Run( migrationStarted = true; - using (var context = new EventProviderDbContext( + using (var context = new ProviderDbContext( entry.FullPath, readOnly: false, ensureCreated: false, @@ -1512,7 +1465,7 @@ private bool VerifyEntryReady(string fullPath) { try { - using var context = new EventProviderDbContext( + using var context = new ProviderDbContext( fullPath, readOnly: true, ensureCreated: false, @@ -1522,7 +1475,7 @@ private bool VerifyEntryReady(string fullPath) } catch (Exception ex) when (ex is not OperationCanceledException) { - SafeLog(() => _traceLogger.Warn( + SafeLog(() => _traceLogger.Warning( $"{nameof(DatabaseService)}.{nameof(VerifyEntryReady)}: '{fullPath}' verification threw: {ex}")); return false; diff --git a/src/EventLogExpert.UI/Services/DebugLogService.cs b/src/EventLogExpert.UI/Services/DebugLogService.cs index 2678b837..d38d8d1a 100644 --- a/src/EventLogExpert.UI/Services/DebugLogService.cs +++ b/src/EventLogExpert.UI/Services/DebugLogService.cs @@ -100,7 +100,7 @@ public void Error([InterpolatedStringHandlerArgument("")] ErrorLogHandler handle WriteTrace(handler.ToStringAndClear(), LogLevel.Error); } - public void Info([InterpolatedStringHandlerArgument("")] InfoLogHandler handler) + public void Information([InterpolatedStringHandlerArgument("")] InformationLogHandler handler) { if (!handler.IsEnabled) { return; } @@ -150,13 +150,23 @@ public void Trace([InterpolatedStringHandlerArgument("")] TraceLogHandler handle WriteTrace(handler.ToStringAndClear(), LogLevel.Trace); } - public void Warn([InterpolatedStringHandlerArgument("")] WarnLogHandler handler) + public void Warning([InterpolatedStringHandlerArgument("")] WarningLogHandler handler) { if (!handler.IsEnabled) { return; } WriteTrace(handler.ToStringAndClear(), LogLevel.Warning); } + private static string DeriveMutexName(string path) + { + // Canonicalize: equivalent paths (relative, separator drift) must hash identically. + var canonical = Path.GetFullPath(path) + .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + var bytes = Encoding.UTF8.GetBytes(canonical.ToUpperInvariant()); + var hash = SHA256.HashData(bytes); + return $"Local\\EventLogExpert.DebugLog.{Convert.ToHexString(hash, 0, 8)}"; + } + private void CloseWriter() { _writer?.Dispose(); @@ -198,42 +208,6 @@ private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) WriteTrace($"Unhandled Exception: {e.ExceptionObject}", LogLevel.Critical); } - private void WriteTrace(string message, LogLevel level) - { - using (_writeLock.EnterScope()) - { - string output = $"[{DateTime.Now:o}] [{Environment.CurrentManagedThreadId}] [{level}] {message}"; - - EnsureWriter(); - - // Mutex serializes line writes so concurrent instances don't interleave. - WithInterprocessLock( - () => - { - if (_writer is null) { return; } - - // Re-seek to EOF: another instance may have written or truncated the file. - _writer.BaseStream.Seek(0, SeekOrigin.End); - _writer.WriteLine(output); - }, - throwOnTimeout: false); - -#if DEBUG - System.Diagnostics.Debug.WriteLine(output); -#endif - } - } - - private static string DeriveMutexName(string path) - { - // Canonicalize: equivalent paths (relative, separator drift) must hash identically. - var canonical = Path.GetFullPath(path) - .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - var bytes = Encoding.UTF8.GetBytes(canonical.ToUpperInvariant()); - var hash = SHA256.HashData(bytes); - return $"Local\\EventLogExpert.DebugLog.{Convert.ToHexString(hash, 0, 8)}"; - } - private void WithInterprocessLock(Action action, bool throwOnTimeout) { bool acquired; @@ -269,4 +243,30 @@ private void WithInterprocessLock(Action action, bool throwOnTimeout) _interprocessMutex.ReleaseMutex(); } } + + private void WriteTrace(string message, LogLevel level) + { + using (_writeLock.EnterScope()) + { + string output = $"[{DateTime.Now:o}] [{Environment.CurrentManagedThreadId}] [{level}] {message}"; + + EnsureWriter(); + + // Mutex serializes line writes so concurrent instances don't interleave. + WithInterprocessLock( + () => + { + if (_writer is null) { return; } + + // Re-seek to EOF: another instance may have written or truncated the file. + _writer.BaseStream.Seek(0, SeekOrigin.End); + _writer.WriteLine(output); + }, + throwOnTimeout: false); + +#if DEBUG + System.Diagnostics.Debug.WriteLine(output); +#endif + } + } } diff --git a/src/EventLogExpert.UI/Services/FilterCategoryItemsCache.cs b/src/EventLogExpert.UI/Services/FilterCategoryItemsCache.cs index ebd050dc..8d7d1efe 100644 --- a/src/EventLogExpert.UI/Services/FilterCategoryItemsCache.cs +++ b/src/EventLogExpert.UI/Services/FilterCategoryItemsCache.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.UI.Models; using System.Collections.Concurrent; using System.Collections.Immutable; @@ -11,8 +11,8 @@ namespace EventLogExpert.UI.Services; /// /// Caches the distinct, sorted list of category values across all active logs, keyed by the -/// snapshot reference. Each snapshot change -/// produces a new key, so cache entries auto-evict via . +/// snapshot reference. Each snapshot change +/// produces a new key, so cache entries auto-evict via . /// public static class FilterCategoryItemsCache { diff --git a/src/EventLogExpert.UI/Services/FilterCompiler.cs b/src/EventLogExpert.UI/Services/FilterCompiler.cs index 88bcce0a..c9855427 100644 --- a/src/EventLogExpert.UI/Services/FilterCompiler.cs +++ b/src/EventLogExpert.UI/Services/FilterCompiler.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.UI.Models; using System.Diagnostics.CodeAnalysis; using System.Linq.Dynamic.Core; diff --git a/src/EventLogExpert.UI/Services/FilterService.cs b/src/EventLogExpert.UI/Services/FilterService.cs index 8ef14a47..745c0963 100644 --- a/src/EventLogExpert.UI/Services/FilterService.cs +++ b/src/EventLogExpert.UI/Services/FilterService.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using System.Linq.Dynamic.Core; diff --git a/src/EventLogExpert.UI/Services/UpdateService.cs b/src/EventLogExpert.UI/Services/UpdateService.cs index 2f31de49..38bf9cfd 100644 --- a/src/EventLogExpert.UI/Services/UpdateService.cs +++ b/src/EventLogExpert.UI/Services/UpdateService.cs @@ -129,7 +129,7 @@ await alertDialogService.ShowAlert("Update Failure", if (string.IsNullOrEmpty(downloadPath)) { - traceLogger.Warn($"{nameof(CheckForUpdates)} Could not get asset download path."); + traceLogger.Warning($"{nameof(CheckForUpdates)} Could not get asset download path."); return; } diff --git a/src/EventLogExpert.UI/Store/EventLog/EventLogAction.cs b/src/EventLogExpert.UI/Store/EventLog/EventLogAction.cs index f6498a5f..47cb9349 100644 --- a/src/EventLogExpert.UI/Store/EventLog/EventLogAction.cs +++ b/src/EventLogExpert.UI/Store/EventLog/EventLogAction.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; -using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Common.Channels; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.UI.Models; using System.Collections.Immutable; @@ -26,7 +26,7 @@ public sealed record LoadEventsPartial(EventLogData LogData, IReadOnlyList _logCts = new(); - private readonly ITraceLogger _logger = logger; /// Tracks per-log load completion so can wait for /// the in-flight service scope to dispose before draining the @@ -69,13 +69,14 @@ public sealed class EventLogEffects( /// service scope (which owns the IEventResolver and its SQLite handles) only disposes /// once HandleOpenLog's outer using/finally runs. private readonly ConcurrentDictionary _logLoadCompletions = new(); + private readonly ILogWatcherService _logWatcherService = logWatcherService; + private readonly ITraceLogger _logger = logger; /// Tracks which currently-open logs (by ) were loaded /// with renderXml=true. A reload-on-transition only re-opens logs that lack XML; logs that /// already have it are left alone. Removing or disabling an XML filter never triggers a /// reload because the XML data is already in memory and harmless to keep. private readonly ConcurrentDictionary _logsLoadedWithXml = new(); - private readonly ILogWatcherService _logWatcherService = logWatcherService; /// Pending selection restore per log name, populated when a filter transition forces /// a reload. Consumed by when the reloaded log finishes loading. @@ -178,7 +179,7 @@ public async Task HandleCloseLog(EventLogAction.CloseLog action, IDispatcher dis } // Drain watcher callbacks. RemoveLogAsync's Task only completes after - // EventLogWatcher.Unsubscribe blocks for all in-flight ProcessNewEvents + // EventLogWatcher.Unsubscribe blocks for all in-flight ReadAndRaiseEvents // callbacks → all per-event resolver scopes disposed → all DbContexts // disposed → connections returned to the SQLite pool. await _logWatcherService.RemoveLogAsync(action.LogName); @@ -188,7 +189,7 @@ public async Task HandleCloseLog(EventLogAction.CloseLog action, IDispatcher dis // Drop any cached XML for this log; if the same name reopens later (e.g., post-rotation) // a fresh resolve must occur instead of returning stale text from a different file. - _xmlResolver.ClearLog(action.LogName); + _xmlResolver.ClearXmlCacheForLog(action.LogName); dispatcher.Dispatch(new EventTableAction.CloseLog(action.LogId)); @@ -591,7 +592,7 @@ public async Task PrepareForDatabaseRemovalAsync(LogReopenSnapshot snapshot, Can // Pre-register the close-completion TCSes BEFORE dispatching, so HandleCloseLog // is guaranteed to find an entry to signal (the dispatcher might queue HandleCloseLog // before this method's next statement runs). - var waiters = new List<(EventLogId Id, string Name, PathType Type, Task Task)>(activeLogs.Count); + var waiters = new List<(EventLogId Id, string Name, LogPathType Type, Task Task)>(activeLogs.Count); foreach (var log in activeLogs) { @@ -742,7 +743,7 @@ private async Task LoadLogAsync( IDispatcher dispatcher, CancellationToken token) { - if (action.PathType == PathType.FilePath) + if (action.LogPathType == LogPathType.File) { try { @@ -760,7 +761,7 @@ private async Task LoadLogAsync( { Array.Sort(mtaFiles, StringComparer.Ordinal); eventResolver.SetMetadataPaths(mtaFiles); - _logger?.Info($"Using locale metadata from: {localeDir} ({mtaFiles.Length} file(s))"); + _logger?.Information($"Using locale metadata from: {localeDir} ({mtaFiles.Length} file(s))"); } } } @@ -768,7 +769,7 @@ private async Task LoadLogAsync( catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or SecurityException or ArgumentException or NotSupportedException) { - _logger?.Warn($"Failed to probe locale metadata for {action.LogName}: {ex.Message}"); + _logger?.Warning($"Failed to probe locale metadata for {action.LogName}: {ex.Message}"); } } @@ -818,7 +819,7 @@ private async Task LoadLogAsync( bool renderXml = _eventLogState.Value.AppliedFilter.RequiresXml; - using var reader = new EventLogReader(action.LogName, action.PathType, renderXml); + using var reader = new EventLogReader(action.LogName, action.LogPathType, renderXml); // Producer: single thread reads batches from EventLogReader var producerTask = Task.Run(async () => @@ -875,7 +876,7 @@ await Parallel.ForEachAsync( { Interlocked.Increment(ref failed); - _logger?.Warn($"{@event.PathName}: Bad Event: {@event.Error}"); + _logger?.Warning($"{@event.PathName}: Bad Event: {@event.Error}"); continue; } @@ -885,7 +886,7 @@ await Parallel.ForEachAsync( } catch (Exception ex) { - _logger?.Warn($"Failed to resolve RecordId: {@event.RecordId}, {ex.Message}"); + _logger?.Warning($"Failed to resolve RecordId: {@event.RecordId}, {ex.Message}"); } } @@ -964,7 +965,7 @@ await Parallel.ForEachAsync( dispatcher.Dispatch(new StatusBarAction.SetEventsLoading(activityId, 0, 0)); - if (action.PathType == PathType.LogName) + if (action.LogPathType == LogPathType.Channel) { _logWatcherService.AddLog(action.LogName, lastEvent, renderXml); } diff --git a/src/EventLogExpert.UI/Store/EventLog/EventLogReducers.cs b/src/EventLogExpert.UI/Store/EventLog/EventLogReducers.cs index fe151475..d6ca953a 100644 --- a/src/EventLogExpert.UI/Store/EventLog/EventLogReducers.cs +++ b/src/EventLogExpert.UI/Store/EventLog/EventLogReducers.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; -using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Common.Channels; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.UI.Models; using Fluxor; using System.Collections.Immutable; @@ -106,7 +106,7 @@ public static EventLogState ReduceOpenLog(EventLogState state, EventLogAction.Op ? state : state with { - ActiveLogs = state.ActiveLogs.Add(action.LogName, GetEmptyLogData(action.LogName, action.PathType)) + ActiveLogs = state.ActiveLogs.Add(action.LogName, GetEmptyLogData(action.LogName, action.LogPathType)) }; [ReducerMethod] @@ -219,6 +219,23 @@ public static EventLogState ReduceSelectEvents(EventLogState state, EventLogActi }; } + [ReducerMethod] + public static EventLogState ReduceSetContinuouslyUpdate( + EventLogState state, + EventLogAction.SetContinuouslyUpdate action) => + state with { ContinuouslyUpdate = action.ContinuouslyUpdate }; + + [ReducerMethod] + public static EventLogState ReduceSetFilters(EventLogState state, EventLogAction.SetFilters action) + { + if (!FilterMethods.HasFilteringChanged(action.EventFilter, state.AppliedFilter)) + { + return state; + } + + return state with { AppliedFilter = action.EventFilter }; + } + [ReducerMethod] public static EventLogState ReduceSetSelectedEvents(EventLogState state, EventLogAction.SetSelectedEvents action) { @@ -267,6 +284,9 @@ private static bool ContainsReference(ImmutableList list, Dis return false; } + private static EventLogData GetEmptyLogData(string logName, LogPathType pathType) => + new(logName, pathType, new List().AsReadOnly()); + private static ImmutableList RemoveByReference( ImmutableList list, DisplayEventModel target) @@ -298,26 +318,6 @@ private static bool SelectionsEqualByReference( return true; } - [ReducerMethod] - public static EventLogState ReduceSetContinuouslyUpdate( - EventLogState state, - EventLogAction.SetContinuouslyUpdate action) => - state with { ContinuouslyUpdate = action.ContinuouslyUpdate }; - - [ReducerMethod] - public static EventLogState ReduceSetFilters(EventLogState state, EventLogAction.SetFilters action) - { - if (!FilterMethods.HasFilteringChanged(action.EventFilter, state.AppliedFilter)) - { - return state; - } - - return state with { AppliedFilter = action.EventFilter }; - } - - private static EventLogData GetEmptyLogData(string logName, PathType pathType) => - new(logName, pathType, new List().AsReadOnly()); - private static EventLogState UpdateActiveLog( EventLogState state, EventLogData logData, diff --git a/src/EventLogExpert.UI/Store/EventLog/EventLogState.cs b/src/EventLogExpert.UI/Store/EventLog/EventLogState.cs index 2d85f9e3..f463c21d 100644 --- a/src/EventLogExpert.UI/Store/EventLog/EventLogState.cs +++ b/src/EventLogExpert.UI/Store/EventLog/EventLogState.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.UI.Models; using Fluxor; using System.Collections.Immutable; diff --git a/src/EventLogExpert.UI/Store/EventLog/ILogReloadCoordinator.cs b/src/EventLogExpert.UI/Store/EventLog/ILogReloadCoordinator.cs index ea499dc0..36f62105 100644 --- a/src/EventLogExpert.UI/Store/EventLog/ILogReloadCoordinator.cs +++ b/src/EventLogExpert.UI/Store/EventLog/ILogReloadCoordinator.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Common.Channels; namespace EventLogExpert.UI.Store.EventLog; @@ -18,7 +18,7 @@ public interface ILogReloadCoordinator void ReopenAfterDatabaseRemoval(IReadOnlyList snapshot); } -public sealed record LogReopenInfo(string Name, PathType Type); +public sealed record LogReopenInfo(string Name, LogPathType Type); /// /// Mutable container that the coordinator populates as each active log finishes closing. Callers pass an empty diff --git a/src/EventLogExpert.UI/Store/EventLog/LiveLogWatcherService.cs b/src/EventLogExpert.UI/Store/EventLog/LogWatcherService.cs similarity index 90% rename from src/EventLogExpert.UI/Store/EventLog/LiveLogWatcherService.cs rename to src/EventLogExpert.UI/Store/EventLog/LogWatcherService.cs index fdc0f7ec..82274ae5 100644 --- a/src/EventLogExpert.UI/Store/EventLog/LiveLogWatcherService.cs +++ b/src/EventLogExpert.UI/Store/EventLog/LogWatcherService.cs @@ -1,9 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventResolvers; using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Resolvers; using Fluxor; using Microsoft.Extensions.DependencyInjection; @@ -18,7 +18,7 @@ public interface ILogWatcherService Task RemoveLogAsync(string logName); } -public sealed class LiveLogWatcherService : ILogWatcherService +public sealed class LogWatcherService : ILogWatcherService { private readonly Dictionary _bookmarks = []; private readonly ITraceLogger _debugLogger; @@ -29,7 +29,7 @@ public sealed class LiveLogWatcherService : ILogWatcherService private readonly Dictionary _watchers = []; private readonly Lock _watchersLock = new(); - public LiveLogWatcherService( + public LogWatcherService( IStateSelection newEventBufferIsFull, ITraceLogger debugLogger, IDispatcher dispatcher, @@ -64,7 +64,7 @@ public void AddLog(string logName, string? bookmark, bool renderXml = false) if (_logsToWatch.Contains(logName)) { throw new InvalidOperationException( - $"Attempted to add log {logName} which is already present in LiveLogWatcher."); + $"Attempted to add log {logName} which is already present in {nameof(LogWatcherService)}."); } _logsToWatch.Add(logName); @@ -119,14 +119,14 @@ public Task RemoveLogAsync(string logName) // Always dispose on a background thread to avoid a deadlock if this is // called on the UI thread. EventLogWatcher.Unsubscribe blocks until all - // in-flight ProcessNewEvents callbacks have completed (so the per-event + // in-flight ReadAndRaiseEvents callbacks have completed (so the per-event // service scopes — and the SQLite handles they hold — are released // before this Task completes). return Task.Run(() => { watcher.Dispose(); - _debugLogger.Info($"{nameof(LiveLogWatcherService)} disposed the old watcher for log {logName}."); + _debugLogger.Information($"{nameof(LogWatcherService)} disposed the old watcher for log {logName}."); }); } @@ -172,7 +172,7 @@ private void StartWatching(string logName) if (eventResolver is null) { - _debugLogger.Warn($"{nameof(LiveLogWatcherService)} event resolver is null in EventRecordWritten callback."); + _debugLogger.Warning($"{nameof(LogWatcherService)} event resolver is null in EventRecordWritten callback."); return; } @@ -200,7 +200,7 @@ private void StartWatching(string logName) { watcher.Enabled = true; - _debugLogger.Info($"{nameof(LiveLogWatcherService)} started watching {logName}."); + _debugLogger.Information($"{nameof(LogWatcherService)} started watching {logName}."); }); } @@ -237,7 +237,7 @@ private Task StopWatchingAsync(string logName) { watcher.Dispose(); - _debugLogger.Info($"{nameof(LiveLogWatcherService)} disposed the old watcher for log {logName}."); + _debugLogger.Information($"{nameof(LogWatcherService)} disposed the old watcher for log {logName}."); }); } } diff --git a/src/EventLogExpert.UI/Store/EventTable/EventTableAction.cs b/src/EventLogExpert.UI/Store/EventTable/EventTableAction.cs index 36078d1e..b1d8feb3 100644 --- a/src/EventLogExpert.UI/Store/EventTable/EventTableAction.cs +++ b/src/EventLogExpert.UI/Store/EventTable/EventTableAction.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.UI.Models; using System.Collections.Immutable; diff --git a/src/EventLogExpert.UI/Store/EventTable/EventTableReducers.cs b/src/EventLogExpert.UI/Store/EventTable/EventTableReducers.cs index c9ed7e2a..dca12c39 100644 --- a/src/EventLogExpert.UI/Store/EventTable/EventTableReducers.cs +++ b/src/EventLogExpert.UI/Store/EventTable/EventTableReducers.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; -using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Common.Channels; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.UI.Models; using Fluxor; using System.Collections.Immutable; @@ -16,9 +16,9 @@ public static EventTableState ReduceAddTable(EventTableState state, EventTableAc { var newTable = new EventTableModel(action.LogData.Id) { - FileName = action.LogData.Type == PathType.LogName ? null : action.LogData.Name, + FileName = action.LogData.Type == LogPathType.Channel ? null : action.LogData.Name, LogName = action.LogData.Name, - PathType = action.LogData.Type, + LogPathType = action.LogData.Type, IsLoading = true }; diff --git a/src/EventLogExpert.UI/Store/EventTable/EventTableState.cs b/src/EventLogExpert.UI/Store/EventTable/EventTableState.cs index 08061cfe..faeb5984 100644 --- a/src/EventLogExpert.UI/Store/EventTable/EventTableState.cs +++ b/src/EventLogExpert.UI/Store/EventTable/EventTableState.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.UI.Models; using Fluxor; using System.Collections.Immutable; diff --git a/src/EventLogExpert.UI/Store/LoggingMiddleware.cs b/src/EventLogExpert.UI/Store/LoggingMiddleware.cs index 2b3bbaed..f2905586 100644 --- a/src/EventLogExpert.UI/Store/LoggingMiddleware.cs +++ b/src/EventLogExpert.UI/Store/LoggingMiddleware.cs @@ -2,7 +2,6 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; -using EventLogExpert.Eventing.Readers; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.EventTable; using EventLogExpert.UI.Store.StatusBar; @@ -29,7 +28,7 @@ public override void BeforeDispatch(object action) _debugLogger.Debug($"Action: {action.GetType()} with {addEventAction.NewEvent.Source} event ID {addEventAction.NewEvent.Id}."); break; case EventLogAction.OpenLog openLogAction: - _debugLogger.Info($"Action: {action.GetType()} with {openLogAction.LogName} log type {openLogAction.PathType}."); + _debugLogger.Information($"Action: {action.GetType()} with {openLogAction.LogName} log type {openLogAction.LogPathType}."); break; case EventLogAction.SelectEvent selectEventAction: _debugLogger.Debug($"Action: {nameof(EventLogAction.SelectEvent)} selected {selectEventAction.SelectedEvent.Source} event ID {selectEventAction.SelectedEvent.Id}."); diff --git a/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs b/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs index 65644ec4..a9a160e9 100644 --- a/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs +++ b/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs @@ -1,10 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventResolvers; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.Eventing.Logging; -using EventLogExpert.Eventing.Models; -using EventLogExpert.UI; +using EventLogExpert.Eventing.Resolvers; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Store.EventLog; using Fluxor; diff --git a/src/EventLogExpert/Components/Sections/EventTable.razor b/src/EventLogExpert/Components/Sections/EventTable.razor index 0f94b9d4..4d134d00 100644 --- a/src/EventLogExpert/Components/Sections/EventTable.razor +++ b/src/EventLogExpert/Components/Sections/EventTable.razor @@ -1,5 +1,4 @@ -@using EventLogExpert.Eventing.Models -@using EventLogExpert.UI +@using EventLogExpert.Eventing.Common.Events @inherits FluxorComponent diff --git a/src/EventLogExpert/Components/Sections/EventTable.razor.cs b/src/EventLogExpert/Components/Sections/EventTable.razor.cs index c254b5e6..cd01ea29 100644 --- a/src/EventLogExpert/Components/Sections/EventTable.razor.cs +++ b/src/EventLogExpert/Components/Sections/EventTable.razor.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.Eventing.Logging; -using EventLogExpert.Eventing.Models; using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; @@ -153,7 +153,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) catch (JSDisconnectedException) { /* Circuit gone — fall back to default page size. */ } catch (Exception e) { - TraceLogger.Warn($"Failed to measure table page size, using default {DefaultPageSize}: {e}"); + TraceLogger.Warning($"Failed to measure table page size, using default {DefaultPageSize}: {e}"); } } @@ -416,7 +416,7 @@ private async Task FocusActiveRow() catch (JSDisconnectedException) { /* Circuit gone — focus best-effort during teardown. */ } catch (Exception e) { - TraceLogger.Warn($"Failed to focus active table row: {e}"); + TraceLogger.Warning($"Failed to focus active table row: {e}"); } } @@ -648,18 +648,6 @@ private async void OnSetActiveTable(EventTableAction.SetActiveTable action) } } - private async void RescrollToSelected() - { - try - { - await InvokeAsync(ScrollToSelectedEvent); - } - catch (Exception e) - { - TraceLogger.Error($"Failed to scroll to selected event: {e}"); - } - } - private void RebuildRowIndexMap() { var displayedEvents = ResolveActiveDisplayedEvents(); @@ -703,6 +691,18 @@ private void RebuildRowIndexMap() } } + private async void RescrollToSelected() + { + try + { + await InvokeAsync(ScrollToSelectedEvent); + } + catch (Exception e) + { + TraceLogger.Error($"Failed to scroll to selected event: {e}"); + } + } + private IReadOnlyList ResolveActiveDisplayedEvents() { if (_currentTable is null) { return []; } @@ -965,7 +965,7 @@ private async Task TryRefreshPageSize() catch (JSDisconnectedException) { return 0; } catch (Exception e) { - TraceLogger.Warn($"Failed to refresh table page size: {e}"); + TraceLogger.Warning($"Failed to refresh table page size: {e}"); return 0; } @@ -987,7 +987,7 @@ private void WarnOnUnknownFilterColors(IEnumerable filters) if (shouldWarn) { - TraceLogger.Warn( + TraceLogger.Warning( $"Unknown HighlightColor value {rawValue} found in filter set; affected filters will be skipped for highlight resolution."); } } diff --git a/src/EventLogExpert/Components/Sections/SplitLogTabPane.razor.cs b/src/EventLogExpert/Components/Sections/SplitLogTabPane.razor.cs index d3013190..7dfa05ad 100644 --- a/src/EventLogExpert/Components/Sections/SplitLogTabPane.razor.cs +++ b/src/EventLogExpert/Components/Sections/SplitLogTabPane.razor.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.EventTable; @@ -51,26 +51,11 @@ .. EventTableState.Value.EventTables return true; } - private string GetTabName(EventTableModel table) - { - if (table.IsCombined) { return "Combined"; } - - string tabName = table.PathType is PathType.FilePath ? - Path.GetFileNameWithoutExtension(table.FileName)!.Split("\\").Last() : - $"{table.LogName} - {table.ComputerName}"; - - if (table.IsLoading) { return tabName; } - - int count = _eventTableState.EventCountByLog.GetValueOrDefault(table.Id, 0); - - return count <= 0 ? $"(Empty) {tabName}" : tabName; - } - private static string GetTabTooltip(EventTableModel table) { if (table.IsCombined) { return string.Empty; } - return $"{(table.PathType == PathType.FilePath ? "Log File: " : "Live Log: ")} {table.FileName}\n" + + return $"{(table.LogPathType == LogPathType.File ? "Log File: " : "Live Log: ")} {table.FileName}\n" + $"Log Name: {table.LogName}\n" + $"Computer Name: {table.ComputerName}"; } @@ -81,6 +66,21 @@ private void CloseLog(EventTableModel table) => private string GetActiveTab(EventTableModel table) => EventTableState.Value.ActiveEventLogId == table.Id ? "tab active" : "tab"; + private string GetTabName(EventTableModel table) + { + if (table.IsCombined) { return "Combined"; } + + string tabName = table.LogPathType is LogPathType.File ? + Path.GetFileNameWithoutExtension(table.FileName)!.Split("\\").Last() : + $"{table.LogName} - {table.ComputerName}"; + + if (table.IsLoading) { return tabName; } + + int count = _eventTableState.EventCountByLog.GetValueOrDefault(table.Id, 0); + + return count <= 0 ? $"(Empty) {tabName}" : tabName; + } + private void SetActiveLog(EventTableModel table) => Dispatcher.Dispatch(new EventTableAction.SetActiveTable(table.Id)); } diff --git a/src/EventLogExpert/Components/Sections/StatusBar.razor b/src/EventLogExpert/Components/Sections/StatusBar.razor index 36b8179b..e549bd8b 100644 --- a/src/EventLogExpert/Components/Sections/StatusBar.razor +++ b/src/EventLogExpert/Components/Sections/StatusBar.razor @@ -1,4 +1,5 @@ -@using EventLogExpert.UI +@using EventLogExpert.Eventing.Common.Channels +@using EventLogExpert.UI @inherits FluxorComponent
@@ -33,7 +34,7 @@ Visible: @(filteredEvents) Hidden by filter: @(totalEvents - filteredEvents) } - @if (_eventLogState.ActiveLogs.Values.Any(l => l.Type == PathType.LogName)) + @if (_eventLogState.ActiveLogs.Values.Any(l => l.Type == LogPathType.Channel)) { @if (_eventLogState.ContinuouslyUpdate) { diff --git a/src/EventLogExpert/MainPage.xaml.cs b/src/EventLogExpert/MainPage.xaml.cs index e24aaf58..60aa1953 100644 --- a/src/EventLogExpert/MainPage.xaml.cs +++ b/src/EventLogExpert/MainPage.xaml.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.Eventing.Logging; -using EventLogExpert.Eventing.Readers; using EventLogExpert.Services; using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; @@ -127,7 +127,7 @@ private async void DropGestureRecognizer_OnDrop(object? sender, DropEventArgs e) var droppedFilePaths = items.OfType() .Where(file => !string.IsNullOrEmpty(file.Path)) - .Select(file => (file.Path, PathType.FilePath)) + .Select(file => (file.Path, LogPathType.File)) .ToList(); if (droppedFilePaths.Count == 0) { return; } @@ -183,7 +183,7 @@ private async Task ProcessCommandLine() { var evtxArgs = Environment.GetCommandLineArgs() .Where(arg => arg.EndsWith(".evtx", StringComparison.OrdinalIgnoreCase)) - .Select(arg => (arg, PathType.FilePath)) + .Select(arg => (arg, LogPathType.File)) .ToList(); if (evtxArgs.Count == 0) { return; } diff --git a/src/EventLogExpert/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index 4f55690f..3f2caf4c 100644 --- a/src/EventLogExpert/MauiProgram.cs +++ b/src/EventLogExpert/MauiProgram.cs @@ -1,9 +1,10 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.Components.Modals.Alerts; -using EventLogExpert.Eventing.EventResolvers; +using EventLogExpert.Eventing.Common.Databases; using EventLogExpert.Eventing.Logging; +using EventLogExpert.Eventing.Resolvers; using EventLogExpert.Services; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Options; @@ -53,7 +54,7 @@ public static MauiApp CreateMauiApp() builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => sp.GetRequiredService()); builder.Services.AddSingleton(sp => sp.GetRequiredService()); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); var fileLocationOptions = new FileLocationOptions(FileSystem.AppDataDirectory); builder.Services.AddSingleton(fileLocationOptions); @@ -83,7 +84,7 @@ public static MauiApp CreateMauiApp() provider.GetRequiredService()); // Provider Services - builder.Services.AddSingleton(static provider => + builder.Services.AddSingleton(static provider => provider.GetRequiredService()); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/EventLogExpert/Services/ClipboardService.cs b/src/EventLogExpert/Services/ClipboardService.cs index 78997430..10b5dfa8 100644 --- a/src/EventLogExpert/Services/ClipboardService.cs +++ b/src/EventLogExpert/Services/ClipboardService.cs @@ -1,9 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventResolvers; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.Eventing.Logging; -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Resolvers; using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Store.EventLog; diff --git a/src/EventLogExpert/Services/MauiMenuActionService.cs b/src/EventLogExpert/Services/MauiMenuActionService.cs index 14f8124e..f57518d1 100644 --- a/src/EventLogExpert/Services/MauiMenuActionService.cs +++ b/src/EventLogExpert/Services/MauiMenuActionService.cs @@ -3,6 +3,7 @@ using EventLogExpert.Components.Modals; using EventLogExpert.Components.Modals.Filters; +using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Readers; using EventLogExpert.Platforms.Windows; @@ -54,7 +55,8 @@ public async Task CheckForUpdatesAsync() { if (!_currentVersionProvider.IsSupportedOS(DeviceInfo.Version)) { - _traceLogger.Warn($"Update API does not work on versions older than 10.0.19041.0"); + _traceLogger.Warning($"Update API does not work on versions older than 10.0.19041.0"); + return; } @@ -115,7 +117,7 @@ public async Task> GetOtherLogNamesAsync() _cachedLogNames = await Task.Run>(() => EventLogSession.GlobalSession.GetLogNames() - .Where(name => !LogNameMethods.HardCodedLiveLogNames.Contains(name)) + .Where(name => !LogChannelMethods.HardCodedLiveChannels.Contains(name)) .OrderBy(name => name, StringComparer.OrdinalIgnoreCase) .ToList()); @@ -151,7 +153,7 @@ public async Task OpenFileAsync(bool combineLog) var paths = files .Where(file => file is not null && !string.IsNullOrEmpty(file.FullPath)) - .Select(file => (file!.FullPath, PathType.FilePath)) + .Select(file => (file!.FullPath, LogPathType.File)) .ToList(); await OpenLogsBatchAsync(paths, combineLog: true); @@ -174,12 +176,12 @@ public async Task OpenFolderAsync(bool combineLog) if (folderPath is null) { return; } - List<(string, PathType)> files; + List<(string, LogPathType)> files; try { files = Directory.EnumerateFiles(folderPath, "*.evtx", SearchOption.TopDirectoryOnly) - .Select(file => (file, PathType.FilePath)) + .Select(file => (file, LogPathType.File)) .ToList(); } catch (Exception ex) when (ex is UnauthorizedAccessException or IOException) @@ -202,9 +204,9 @@ public async Task OpenFolderAsync(bool combineLog) public Task OpenIssueAsync() => OpenBrowserAsync("https://github.com/microsoft/EventLogExpert/issues/new"); public Task OpenLiveLogAsync(string logName, bool combineLog) => - OpenLogsBatchAsync([(logName, PathType.LogName)], combineLog); + OpenLogsBatchAsync([(logName, LogPathType.Channel)], combineLog); - public async Task OpenLogAsync(string logPath, PathType pathType, bool combineLog = false) + public async Task OpenLogAsync(string logPath, LogPathType pathType, bool combineLog = false) { if (string.IsNullOrWhiteSpace(logPath) || (combineLog && _eventLogState.Value.ActiveLogs.ContainsKey(logPath))) { return OpenLogStatus.Loaded; } @@ -257,7 +259,7 @@ await _dialogService.ShowAlert( /// gesture (multi-file picker, folder open, drag-drop, command line) so the user sees one batched alert instead /// of one popup per empty file. ///
- public async Task OpenLogsBatchAsync(IEnumerable<(string Path, PathType Type)> logs, bool combineLog) + public async Task OpenLogsBatchAsync(IEnumerable<(string Path, LogPathType Type)> logs, bool combineLog) { ArgumentNullException.ThrowIfNull(logs); @@ -323,9 +325,9 @@ await _modalService.Show( public void ToggleShowAllEvents() => _dispatcher.Dispatch(new FilterPaneAction.ToggleIsEnabled()); - private static string GetEmptyLogDisplayName(string path, PathType type) + private static string GetEmptyLogDisplayName(string path, LogPathType type) { - if (type != PathType.FilePath) { return path; } + if (type != LogPathType.File) { return path; } var fileName = Path.GetFileName(path); diff --git a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/EventMessageProviderIntegrationTests.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/EventMessageProviderIntegrationTests.cs index fef112dd..83ca3034 100644 --- a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/EventMessageProviderIntegrationTests.cs +++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/EventMessageProviderIntegrationTests.cs @@ -15,7 +15,7 @@ namespace EventLogExpert.Eventing.IntegrationTests.Providers; public sealed class EventMessageProviderIntegrationTests { [Fact] - public void GetMessages_WhenBinaryUsesMuiSatellite_ShouldLoadMessagesFromMuiFile() + public void LoadMessagesFromFiles_WhenBinaryUsesMuiSatellite_ShouldLoadMessagesFromMuiFile() { // wevtsvc.dll keeps its message table in the .mui satellite — the loader must follow MUI fallback. var systemDirectory = Environment.SystemDirectory; @@ -26,7 +26,7 @@ public void GetMessages_WhenBinaryUsesMuiSatellite_ShouldLoadMessagesFromMuiFile "Test requires wevtsvc.dll and a matching .mui satellite in the loader's MUI fallback chain (current UI culture or en-US)."); // Act - var messages = EventMessageProvider.GetMessages([muiBinary], Constants.TestProviderName); + var messages = EventMessageProvider.LoadMessagesFromFiles([muiBinary], Constants.TestProviderName); // Assert Assert.NotNull(messages); @@ -35,7 +35,7 @@ public void GetMessages_WhenBinaryUsesMuiSatellite_ShouldLoadMessagesFromMuiFile } [Fact] - public void GetMessages_WhenFilePathContainsEnvironmentVariable_ShouldHandleCorrectly() + public void LoadMessagesFromFiles_WhenFilePathContainsEnvironmentVariable_ShouldHandleCorrectly() { // LoadLibraryEx does not expand env vars; provider must expand %SystemRoot% before loading. var systemDirectory = Environment.SystemDirectory; @@ -47,7 +47,7 @@ public void GetMessages_WhenFilePathContainsEnvironmentVariable_ShouldHandleCorr var filesWithEnvVar = new[] { @"%SystemRoot%\System32\wevtsvc.dll" }; - var messages = EventMessageProvider.GetMessages(filesWithEnvVar, Constants.TestProviderName); + var messages = EventMessageProvider.LoadMessagesFromFiles(filesWithEnvVar, Constants.TestProviderName); Assert.NotNull(messages); Assert.NotEmpty(messages); @@ -68,6 +68,22 @@ 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() { @@ -101,22 +117,6 @@ 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() { @@ -133,6 +133,26 @@ public void LoadProviderDetails_WhenChannelOwningPublisherUnknown_ShouldReturnEm Assert.Equal(MadeUpName, details.ProviderName); } + [Fact] + public void LoadProviderDetails_WhenLegacyLoadReturnsNoMessages_ShouldFallBackToModernMessageFilePath() + { + 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."); + + var mockLogger = Substitute.For(); + EventMessageProvider provider = new(providerName!, logger: mockLogger); + + var details = provider.LoadProviderDetails(); + + Assert.NotNull(details); + Assert.NotEmpty(details.Messages); + + mockLogger.Received().Debug(Arg.Is(h => + h.ToString().Contains("No legacy messages loaded for provider") && + h.ToString().Contains("Using message file from modern provider"))); + } + [Fact] public void LoadProviderDetails_WhenProviderHasNoData_ShouldReturnEmptyCollections() { @@ -258,6 +278,62 @@ private static bool TryFindMuiSatellite(string systemDirectory, string binaryNam return false; } + private static bool TryFindProviderWithEmptyLegacyAndWorkingModern(out string? providerName) + { + var registry = new RegistryProvider(); + + foreach (var candidate in EventLogSession.GlobalSession.GetProviderNames()) + { + var legacyFiles = registry.GetMessageFilesForLegacyProvider(candidate).ToList(); + + if (legacyFiles.Count == 0) + { + continue; + } + + // Probe: confirm the legacy load actually produces zero messages. + // Common when registry points to %SystemRoot%\System32\foo.dll for software that's been uninstalled. + var legacyMessages = EventMessageProvider.LoadMessagesFromFiles(legacyFiles, candidate); + + if (legacyMessages.Count > 0) + { + continue; + } + + ProviderMetadata? metadata; + + try + { + metadata = ProviderMetadata.Create(candidate); + } + catch + { + continue; + } + + if (metadata is null || string.IsNullOrEmpty(metadata.MessageFilePath)) + { + continue; + } + + // Probe: confirm the modern path actually loads messages — otherwise the SUT's fallback would also be empty. + var modernMessages = EventMessageProvider.LoadMessagesFromFiles([metadata.MessageFilePath], candidate); + + if (modernMessages.Count == 0) + { + continue; + } + + providerName = candidate; + + return true; + } + + providerName = null; + + return false; + } + private static bool TryReadOwningPublisher(EvtHandle channelConfig, out string? publisher) { publisher = null; diff --git a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs index 67c43deb..1d799be8 100644 --- a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs +++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs @@ -1,10 +1,10 @@ // // Copyright (c) Microsoft Corporation. // // 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.Readers; using Microsoft.Win32; using NSubstitute; @@ -43,26 +43,6 @@ 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() { @@ -94,6 +74,26 @@ 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)] @@ -478,7 +478,7 @@ private static (string? Name, List Files) FindLegacyProviderWithSemicolo foreach (var logName in eventLogKey.GetSubKeyNames()) { - if (LogNames.AdminOnlyLiveLogNames.Contains(logName)) + if (LogChannelNames.AdminOnlyLiveChannels.Contains(logName)) { continue; } diff --git a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogInformationTests.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogInformationTests.cs index a23d7f17..ac3a777b 100644 --- a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogInformationTests.cs +++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogInformationTests.cs @@ -1,6 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.Eventing.IntegrationTests.TestUtils.Constants; using EventLogExpert.Eventing.Readers; @@ -15,7 +16,7 @@ public void Constructor_WhenApplicationLog_ShouldPopulateAllProperties() var session = EventLogSession.GlobalSession; // Act - var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, PathType.LogName); + var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, LogPathType.Channel); // Assert Assert.NotNull(logInfo); @@ -36,7 +37,7 @@ public void Constructor_WhenBlankLogName_ShouldThrowArgumentException(string log // Act & Assert Assert.Throws(() => - new EventLogInformation(session, logName, PathType.LogName)); + new EventLogInformation(session, logName, LogPathType.Channel)); } [Theory] @@ -48,7 +49,7 @@ public void Constructor_WhenCommonLogs_ShouldHaveRecords(string logName) var session = EventLogSession.GlobalSession; // Act - var logInfo = new EventLogInformation(session, logName, PathType.LogName); + var logInfo = new EventLogInformation(session, logName, LogPathType.Channel); // Assert Assert.NotNull(logInfo.RecordCount); @@ -65,7 +66,7 @@ public void Constructor_WhenCommonLogs_ShouldNotThrow(string logName) var session = EventLogSession.GlobalSession; // Act - var logInfo = new EventLogInformation(session, logName, PathType.LogName); + var logInfo = new EventLogInformation(session, logName, LogPathType.Channel); // Assert Assert.NotNull(logInfo); @@ -80,7 +81,7 @@ public async Task Constructor_WhenConcurrentAccess_ShouldHandleMultipleThreads() // Act var tasks = logNames.Select(logName => - Task.Run(() => new EventLogInformation(session, logName, PathType.LogName)) + Task.Run(() => new EventLogInformation(session, logName, LogPathType.Channel)) ).ToArray(); await Task.WhenAll(tasks); @@ -98,7 +99,7 @@ public async Task Constructor_WhenConcurrentAccess_ShouldHandleMultipleThreads() public void Constructor_WhenGlobalSession_ShouldUseCorrectSession() { // Arrange & Act - var logInfo = EventLogSession.GlobalSession.GetLogInformation(Constants.ApplicationLogName, PathType.LogName); + var logInfo = EventLogSession.GlobalSession.GetLogInformation(Constants.ApplicationLogName, LogPathType.Channel); // Assert Assert.NotNull(logInfo); @@ -114,7 +115,7 @@ public void Constructor_WhenInvalidLogName_ShouldThrowFileNotFoundException() // Act & Assert Assert.Throws(() => - new EventLogInformation(session, invalidLogName, PathType.LogName)); + new EventLogInformation(session, invalidLogName, LogPathType.Channel)); } [Fact] @@ -125,7 +126,7 @@ public void Constructor_WhenNullLogName_ShouldThrowArgumentNullException() // Act & Assert Assert.Throws(() => - new EventLogInformation(session, null!, PathType.LogName)); + new EventLogInformation(session, null!, LogPathType.Channel)); } [Fact] @@ -135,7 +136,7 @@ public void Constructor_WhenPathTypeLogName_ShouldSucceed() var session = EventLogSession.GlobalSession; // Act - var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, PathType.LogName); + var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, LogPathType.Channel); // Assert Assert.NotNull(logInfo); @@ -151,7 +152,7 @@ public void Constructor_WhenSpecialCharactersInLogName_ShouldNotMaskAsUnauthoriz // Act var ex = Record.Exception(() => - new EventLogInformation(session, invalidLogName, PathType.LogName)); + new EventLogInformation(session, invalidLogName, LogPathType.Channel)); // Assert — malformed paths must not be masked as UAE. Assert.NotNull(ex); @@ -165,7 +166,7 @@ public void Constructor_WhenValidLog_AllPropertiesShouldBeAccessible() var session = EventLogSession.GlobalSession; // Act - var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, PathType.LogName); + var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, LogPathType.Channel); // Assert - Verify we can access all properties without exceptions var attributes = logInfo.Attributes; @@ -191,7 +192,7 @@ public void Constructor_WhenValidLog_ShouldHaveNonNegativeRecordCount() var session = EventLogSession.GlobalSession; // Act - var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, PathType.LogName); + var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, LogPathType.Channel); // Assert Assert.NotNull(logInfo.RecordCount); @@ -205,7 +206,7 @@ public void Constructor_WhenValidLog_ShouldHavePositiveFileSize() var session = EventLogSession.GlobalSession; // Act - var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, PathType.LogName); + var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, LogPathType.Channel); // Assert Assert.NotNull(logInfo.FileSize); @@ -219,7 +220,7 @@ public void CreationTime_WhenApplicationLog_ShouldBeValidDateTime() var session = EventLogSession.GlobalSession; // Act - var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, PathType.LogName); + var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, LogPathType.Channel); // Assert if (logInfo.CreationTime.HasValue) @@ -236,7 +237,7 @@ public void FileSize_WhenApplicationLog_ShouldBeReasonable() var session = EventLogSession.GlobalSession; // Act - var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, PathType.LogName); + var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, LogPathType.Channel); // Assert Assert.NotNull(logInfo.FileSize); @@ -253,8 +254,8 @@ public void FileSize_WhenMultipleLogs_ShouldVaryByLog() var session = EventLogSession.GlobalSession; // Act - var appLogInfo = new EventLogInformation(session, Constants.ApplicationLogName, PathType.LogName); - var sysLogInfo = new EventLogInformation(session, Constants.SystemLogName, PathType.LogName); + var appLogInfo = new EventLogInformation(session, Constants.ApplicationLogName, LogPathType.Channel); + var sysLogInfo = new EventLogInformation(session, Constants.SystemLogName, LogPathType.Channel); // Assert Assert.NotNull(appLogInfo.FileSize); @@ -271,8 +272,8 @@ public void GetLogInformation_WhenCalledTwice_ShouldReturnSimilarData() var session = EventLogSession.GlobalSession; // Act - var logInfo1 = session.GetLogInformation(Constants.ApplicationLogName, PathType.LogName); - var logInfo2 = session.GetLogInformation(Constants.ApplicationLogName, PathType.LogName); + var logInfo1 = session.GetLogInformation(Constants.ApplicationLogName, LogPathType.Channel); + var logInfo2 = session.GetLogInformation(Constants.ApplicationLogName, LogPathType.Channel); // Assert // File size might change slightly, but should be close @@ -290,7 +291,7 @@ public void IsLogFull_WhenApplicationLog_ShouldBeFalse() var session = EventLogSession.GlobalSession; // Act - var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, PathType.LogName); + var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, LogPathType.Channel); // Assert Assert.NotNull(logInfo.IsLogFull); @@ -305,7 +306,7 @@ public void LastAccessTime_WhenApplicationLog_ShouldBeValidOrNull() var session = EventLogSession.GlobalSession; // Act - var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, PathType.LogName); + var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, LogPathType.Channel); // Assert if (logInfo.LastAccessTime.HasValue) @@ -322,7 +323,7 @@ public void LastWriteTime_WhenApplicationLog_ShouldBeValidOrNull() var session = EventLogSession.GlobalSession; // Act - var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, PathType.LogName); + var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, LogPathType.Channel); // Assert if (logInfo.LastWriteTime.HasValue) @@ -339,7 +340,7 @@ public void OldestRecordNumber_WhenApplicationLog_ShouldBeNonNegative() var session = EventLogSession.GlobalSession; // Act - var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, PathType.LogName); + var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, LogPathType.Channel); // Assert if (logInfo.OldestRecordNumber.HasValue) @@ -355,7 +356,7 @@ public void Properties_WhenApplicationLog_ShouldBeReadOnly() var session = EventLogSession.GlobalSession; // Act - var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, PathType.LogName); + var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, LogPathType.Channel); // Assert // Properties should be read-only (init-only or get-only) @@ -384,7 +385,7 @@ public void RecordCount_WhenApplicationLog_ShouldBeConsistentWithOldestRecord() var session = EventLogSession.GlobalSession; // Act - var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, PathType.LogName); + var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, LogPathType.Channel); // Assert Assert.NotNull(logInfo.RecordCount); @@ -404,7 +405,7 @@ public void RecordCount_WhenEmptyLog_CanBeZero() // Act - Try to find a log that might be empty // Most logs will have records, but this tests that zero is a valid value - var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, PathType.LogName); + var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, LogPathType.Channel); // Assert Assert.NotNull(logInfo.RecordCount); @@ -419,7 +420,7 @@ public void RecordCount_WhenLogHasRecords_ShouldMatchWithOldestRecordNumber() var session = EventLogSession.GlobalSession; // Act - var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, PathType.LogName); + var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, LogPathType.Channel); // Assert Assert.NotNull(logInfo.RecordCount); diff --git a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogReaderTests.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogReaderTests.cs index 9687ef2e..41b35138 100644 --- a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogReaderTests.cs +++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogReaderTests.cs @@ -1,6 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.Eventing.IntegrationTests.TestUtils.Constants; using EventLogExpert.Eventing.Readers; @@ -12,7 +13,7 @@ public sealed class EventLogReaderTests public void Constructor_WhenApplicationLog_ShouldNotThrow() { // Arrange & Act - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); // Assert Assert.NotNull(reader); @@ -22,7 +23,7 @@ public void Constructor_WhenApplicationLog_ShouldNotThrow() public void Constructor_WhenEmptyLogName_ShouldFailToReadEvents() { // Arrange & Act - using var reader = new EventLogReader(string.Empty, PathType.LogName); + using var reader = new EventLogReader(string.Empty, LogPathType.Channel); // Assert - TryGetEvents must fail bool success = reader.TryGetEvents(out var events); @@ -39,7 +40,7 @@ public void Constructor_WhenInvalidLog_ShouldFailToReadEvents() var invalidLogName = "NonExistentLog_" + Guid.NewGuid(); // Act - using var reader = new EventLogReader(invalidLogName, PathType.LogName); + using var reader = new EventLogReader(invalidLogName, LogPathType.Channel); // Assert - TryGetEvents must fail bool success = reader.TryGetEvents(out var events); @@ -53,8 +54,8 @@ public void Constructor_WhenInvalidLog_ShouldFailToReadEvents() public void Constructor_WhenMultipleInstances_ShouldCreateIndependentReaders() { // Arrange & Act - using var reader1 = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); - using var reader2 = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader1 = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); + using var reader2 = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); // Assert Assert.NotNull(reader1); @@ -66,7 +67,7 @@ public void Constructor_WhenMultipleInstances_ShouldCreateIndependentReaders() public void Constructor_WhenNullLogName_ShouldFailToReadEvents() { // Arrange & Act - using var reader = new EventLogReader(null!, PathType.LogName); + using var reader = new EventLogReader(null!, LogPathType.Channel); // Assert - TryGetEvents must fail bool success = reader.TryGetEvents(out var events); @@ -80,21 +81,21 @@ public void Constructor_WhenNullLogName_ShouldFailToReadEvents() public void Constructor_WhenPathTypeLogName_ShouldQueryByLogName() { // Arrange & Act - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); // Act bool success = reader.TryGetEvents(out var events); // Assert Assert.True(success); - Assert.All(events, evt => Assert.Equal(PathType.LogName, evt.PathType)); + 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, PathType.LogName, renderXml: false); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel, renderXml: false); // Assert Assert.NotNull(reader); @@ -104,7 +105,7 @@ public void Constructor_WhenRenderXmlFalse_ShouldNotThrow() public void Constructor_WhenRenderXmlTrue_ShouldNotThrow() { // Arrange & Act - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName, renderXml: true); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel, renderXml: true); // Assert Assert.NotNull(reader); @@ -117,7 +118,7 @@ public void Constructor_WhenSpecialCharactersInLogName_ShouldFailToReadEvents() var invalidLogName = "Invalid<>Log|Name"; // Act - using var reader = new EventLogReader(invalidLogName, PathType.LogName); + using var reader = new EventLogReader(invalidLogName, LogPathType.Channel); // Assert - TryGetEvents must fail bool success = reader.TryGetEvents(out var events); @@ -131,7 +132,7 @@ public void Constructor_WhenSpecialCharactersInLogName_ShouldFailToReadEvents() public void Dispose_AfterDispose_TryGetEventsShouldThrow() { // Arrange - var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); // Act reader.Dispose(); @@ -141,25 +142,25 @@ public void Dispose_AfterDispose_TryGetEventsShouldThrow() } [Fact] - public void Dispose_WhenCalled_ShouldNotThrow() + public void Dispose_WhenCalledMultipleTimes_ShouldNotThrow() { // Arrange - var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); // Act & Assert reader.Dispose(); + reader.Dispose(); + reader.Dispose(); } [Fact] - public void Dispose_WhenCalledMultipleTimes_ShouldNotThrow() + public void Dispose_WhenCalled_ShouldNotThrow() { // Arrange - var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); // Act & Assert reader.Dispose(); - reader.Dispose(); - reader.Dispose(); } [Fact] @@ -167,7 +168,7 @@ public void EndOfResults_AfterExhaustion_ShouldKeepBookmarkAndNotSetLastErrorCod { // Arrange — bounded fixture keeps the exhaustion loop fast on any host. using var fixture = new SmallEvtxFixture(); - using var reader = new EventLogReader(fixture.FilePath, PathType.FilePath); + using var reader = new EventLogReader(fixture.FilePath, LogPathType.File); // Act — exhaust the reader so TryGetEvents returns false with ERROR_NO_MORE_ITEMS while (reader.TryGetEvents(out _)) { } @@ -186,7 +187,7 @@ public void EndOfResults_AfterExhaustion_ShouldKeepBookmarkAndNotSetLastErrorCod public void IsValid_WhenApplicationLog_ShouldBeTrue() { // Arrange & Act - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); // Assert Assert.True(reader.IsValid); @@ -196,7 +197,7 @@ public void IsValid_WhenApplicationLog_ShouldBeTrue() public void IsValid_WhenEmptyLogName_ShouldBeFalse() { // Arrange & Act - using var reader = new EventLogReader(string.Empty, PathType.LogName); + using var reader = new EventLogReader(string.Empty, LogPathType.Channel); // Assert Assert.False(reader.IsValid); @@ -209,7 +210,7 @@ public void IsValid_WhenInvalidLogName_ShouldBeFalse() var invalidLogName = "NonExistentLog_" + Guid.NewGuid(); // Act - using var reader = new EventLogReader(invalidLogName, PathType.LogName); + using var reader = new EventLogReader(invalidLogName, LogPathType.Channel); // Assert Assert.False(reader.IsValid); @@ -220,7 +221,7 @@ public void LastBookmark_AfterTryGetEvents_ShouldBeSet() { // Arrange — fixture keeps the read deterministic on minimal hosts. using var fixture = new SmallEvtxFixture(); - using var reader = new EventLogReader(fixture.FilePath, PathType.FilePath); + using var reader = new EventLogReader(fixture.FilePath, LogPathType.File); // Act bool success = reader.TryGetEvents(out var events); @@ -236,7 +237,7 @@ public void LastBookmark_AfterTryGetEvents_ShouldBeSet() public void LastBookmark_WhenInitialized_ShouldBeNull() { // Arrange & Act - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); // Assert Assert.Null(reader.LastBookmark); @@ -248,7 +249,7 @@ public void LastBookmark_WhenMultipleBatches_ShouldUpdateWithEachBatch() // Use SmallEvtxFixture (5 events) with batchSize: 1 so two consecutive // reads are guaranteed to produce two distinct events on every host. using var fixture = new SmallEvtxFixture(); - using var reader = new EventLogReader(fixture.FilePath, PathType.FilePath); + using var reader = new EventLogReader(fixture.FilePath, LogPathType.File); // Act bool success1 = reader.TryGetEvents(out var events1, batchSize: 1); @@ -273,7 +274,7 @@ public void LastBookmark_WhenMultipleBatches_ShouldUpdateWithEachBatch() public void LastErrorCode_WhenInitialized_ShouldBeNull() { // Arrange & Act - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); // Assert Assert.Null(reader.LastErrorCode); @@ -284,7 +285,7 @@ public void LastErrorCode_WhenInvalidHandle_ShouldBeSet() { // Arrange — opening a non-existent log produces an invalid handle var invalidLogName = "NonExistentLog_" + Guid.NewGuid(); - using var reader = new EventLogReader(invalidLogName, PathType.LogName); + using var reader = new EventLogReader(invalidLogName, LogPathType.Channel); // Act reader.TryGetEvents(out _); @@ -297,7 +298,7 @@ public void LastErrorCode_WhenInvalidHandle_ShouldBeSet() public void TryGetEvents_WhenApplicationLog_ShouldReturnEvents() { // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); // Act bool success = reader.TryGetEvents(out var events); @@ -312,7 +313,7 @@ public void TryGetEvents_WhenApplicationLog_ShouldReturnEvents() public void TryGetEvents_WhenApplicationLog_ShouldReturnValidEventRecords() { // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); // Act bool success = reader.TryGetEvents(out var events); @@ -323,46 +324,46 @@ public void TryGetEvents_WhenApplicationLog_ShouldReturnValidEventRecords() { Assert.NotNull(evt); Assert.Equal(Constants.ApplicationLogName, evt.PathName); - Assert.Equal(PathType.LogName, evt.PathType); + Assert.Equal(LogPathType.Channel, evt.LogPathType); }); } [Fact] - public void TryGetEvents_WhenBatchSize1_ShouldReturn1Event() + public void TryGetEvents_WhenBatchSize10_ShouldReturnUpTo10Events() { // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + 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] - public void TryGetEvents_WhenBatchSize10_ShouldReturnUpTo10Events() + public void TryGetEvents_WhenBatchSize1_ShouldReturn1Event() { // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + 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_WhenBatchSize30_ShouldReturnUpTo30Events() { // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); // Act bool success = reader.TryGetEvents(out var events, batchSize: 30); @@ -377,7 +378,7 @@ public void TryGetEvents_WhenBatchSize30_ShouldReturnUpTo30Events() public void TryGetEvents_WhenCalledMultipleTimes_ShouldReturnDifferentEvents() { // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); // Act bool success1 = reader.TryGetEvents(out var events1, batchSize: 5); @@ -402,7 +403,7 @@ public void TryGetEvents_WhenCalledMultipleTimes_ShouldReturnDifferentEvents() public void TryGetEvents_WhenCommonLogs_ShouldReturnEvents(string logName) { // Arrange - using var reader = new EventLogReader(logName, PathType.LogName); + using var reader = new EventLogReader(logName, LogPathType.Channel); // Act bool success = reader.TryGetEvents(out var events); @@ -421,17 +422,17 @@ public async Task TryGetEvents_WhenConcurrentReaders_ShouldHandleMultipleReaders { Task.Run(() => { - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); return reader.TryGetEvents(out _); }), Task.Run(() => { - using var reader = new EventLogReader(Constants.SystemLogName, PathType.LogName); + using var reader = new EventLogReader(Constants.SystemLogName, LogPathType.Channel); return reader.TryGetEvents(out _); }), Task.Run(() => { - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); return reader.TryGetEvents(out _); }) }; @@ -450,7 +451,7 @@ public async Task TryGetEvents_WhenConcurrentReaders_ShouldHandleMultipleReaders public void TryGetEvents_WhenDefaultBatchSize_ShouldReturn30OrLess() { // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); // Act bool success = reader.TryGetEvents(out var events); @@ -465,7 +466,7 @@ public void TryGetEvents_WhenDefaultBatchSize_ShouldReturn30OrLess() public void TryGetEvents_WhenEventsReturned_ShouldHavePathNameSet() { // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); // Act bool success = reader.TryGetEvents(out var events); @@ -483,7 +484,7 @@ public void TryGetEvents_WhenEventsReturned_ShouldHavePathNameSet() public void TryGetEvents_WhenEventsReturned_ShouldHavePathTypeSet() { // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); // Act bool success = reader.TryGetEvents(out var events); @@ -493,7 +494,7 @@ public void TryGetEvents_WhenEventsReturned_ShouldHavePathTypeSet() Assert.All(events, evt => { - Assert.Equal(PathType.LogName, evt.PathType); + Assert.Equal(LogPathType.Channel, evt.LogPathType); }); } @@ -501,7 +502,7 @@ public void TryGetEvents_WhenEventsReturned_ShouldHavePathTypeSet() public void TryGetEvents_WhenEventsReturned_ShouldHaveProperties() { // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); // Act bool success = reader.TryGetEvents(out var events, batchSize: 1); @@ -521,12 +522,12 @@ public void TryGetEvents_WhenEventsReturned_ShouldHaveProperties() public void TryGetEvents_WhenLargeLogWithManyReads_ShouldEventuallyReturnFalse() { // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); var logInfo = new EventLogInformation( EventLogSession.GlobalSession, Constants.ApplicationLogName, - PathType.LogName); + LogPathType.Channel); // Derive iteration limit from actual log size (batch size defaults to 30) long maxIterations = (logInfo.RecordCount ?? 0) / 30 + 100; @@ -551,8 +552,8 @@ public void TryGetEvents_WhenLargeLogWithManyReads_ShouldEventuallyReturnFalse() public void TryGetEvents_WhenMultipleReaders_ShouldReadIndependently() { // Arrange - using var reader1 = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); - using var reader2 = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader1 = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); + using var reader2 = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); // Act bool success1 = reader1.TryGetEvents(out var events1, batchSize: 5); @@ -574,7 +575,7 @@ public void TryGetEvents_WhenMultipleReaders_ShouldReadIndependently() public void TryGetEvents_WhenNoMoreEvents_ShouldReturnFalse() { // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); // Act - Read all events bool success; @@ -596,7 +597,7 @@ public void TryGetEvents_WhenNoMoreEvents_ShouldReturnFalse() public void TryGetEvents_WhenRenderXmlFalse_ShouldNotIncludeXml() { // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName, renderXml: false); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel, renderXml: false); // Act bool success = reader.TryGetEvents(out var events); @@ -617,7 +618,7 @@ public void TryGetEvents_WhenRenderXmlFalse_ShouldNotIncludeXml() public void TryGetEvents_WhenRenderXmlTrue_ShouldIncludeXml() { // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName, renderXml: true); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel, renderXml: true); // Act bool success = reader.TryGetEvents(out var events); @@ -636,7 +637,7 @@ public void TryGetEvents_WhenRenderXmlTrue_ShouldIncludeXml() public void TryGetEvents_WhenRenderXmlTrue_XmlShouldBeValidFormat() { // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName, renderXml: true); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel, renderXml: true); // Act bool success = reader.TryGetEvents(out var events, batchSize: 1); @@ -657,7 +658,7 @@ public void TryGetEvents_WhenRenderXmlTrue_XmlShouldBeValidFormat() public void TryGetEvents_WhenZeroBatchSize_ShouldHandleGracefully() { // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); // Act bool success = reader.TryGetEvents(out var events, batchSize: 0); diff --git a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogSessionTests.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogSessionTests.cs index 9a049234..6cdd0870 100644 --- a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogSessionTests.cs +++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogSessionTests.cs @@ -1,6 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.Eventing.IntegrationTests.TestUtils.Constants; using EventLogExpert.Eventing.Readers; @@ -15,7 +16,7 @@ public void GetLogInformation_WhenApplicationLog_ShouldReturnInformation() var session = EventLogSession.GlobalSession; // Act - var logInfo = session.GetLogInformation(Constants.ApplicationLogName, PathType.LogName); + var logInfo = session.GetLogInformation(Constants.ApplicationLogName, LogPathType.Channel); // Assert Assert.NotNull(logInfo); @@ -31,7 +32,7 @@ public void GetLogInformation_WhenCommonLogs_ShouldReturnInformation(string logN var session = EventLogSession.GlobalSession; // Act - var logInfo = session.GetLogInformation(logName, PathType.LogName); + var logInfo = session.GetLogInformation(logName, LogPathType.Channel); // Assert Assert.NotNull(logInfo); @@ -47,9 +48,9 @@ public async Task GetLogInformation_WhenConcurrentAccess_ShouldHandleMultipleThr // Act var tasks = new[] { - Task.Run(() => session.GetLogInformation(Constants.ApplicationLogName, PathType.LogName)), - Task.Run(() => session.GetLogInformation(Constants.SystemLogName, PathType.LogName)), - Task.Run(() => session.GetLogInformation(Constants.ApplicationLogName, PathType.LogName)) + Task.Run(() => session.GetLogInformation(Constants.ApplicationLogName, LogPathType.Channel)), + Task.Run(() => session.GetLogInformation(Constants.SystemLogName, LogPathType.Channel)), + Task.Run(() => session.GetLogInformation(Constants.ApplicationLogName, LogPathType.Channel)) }; await Task.WhenAll(tasks); @@ -73,7 +74,7 @@ public void GetLogInformation_WhenInvalidLogName_ShouldThrowFileNotFoundExceptio // Surfaces the real EvtOpenLog error (ERROR_EVT_CHANNEL_NOT_FOUND) instead // of the previous masked UnauthorizedAccessException. Assert.Throws(() => - session.GetLogInformation(invalidLogName, PathType.LogName)); + session.GetLogInformation(invalidLogName, LogPathType.Channel)); } [Fact] @@ -91,6 +92,26 @@ 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() { @@ -194,26 +215,6 @@ 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() { @@ -253,6 +254,26 @@ 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() { @@ -328,26 +349,6 @@ 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() { @@ -373,26 +374,26 @@ public async Task GetProviderNames_WhenConcurrentAccess_ShouldHandleMultipleThre } [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] - 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] @@ -404,7 +405,7 @@ public void GlobalSession_WhenUsedByMultipleMethods_ShouldWorkCorrectly() // Act var logNames = session.GetLogNames().ToList(); var providers = session.GetProviderNames(); - var logInfo = session.GetLogInformation(Constants.ApplicationLogName, PathType.LogName); + var logInfo = session.GetLogInformation(Constants.ApplicationLogName, LogPathType.Channel); // Assert Assert.NotEmpty(logNames); diff --git a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs index f8b47d4b..2406e1fe 100644 --- a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs +++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.Eventing.IntegrationTests.TestUtils.Constants; -using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Readers; using System.Collections.Concurrent; using System.Diagnostics; @@ -54,16 +54,6 @@ public void Constructor_WithInvalidLogName_ShouldThrowFileNotFoundException() Assert.Throws(() => new EventLogWatcher(invalidLogName)); } - [Fact] - public void Constructor_WithLogName_ShouldCreateWatcher() - { - // Arrange & Act - using var watcher = new EventLogWatcher(Constants.ApplicationLogName); - - // Assert - Assert.NotNull(watcher); - } - [Fact] public void Constructor_WithLogNameAndRenderXml_ShouldCreateWatcher() { @@ -87,6 +77,16 @@ public void Constructor_WithLogNameBookmarkAndRenderXml_ShouldCreateWatcher() 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() { @@ -108,7 +108,7 @@ public void Constructor_WithNullLogName_ShouldThrowArgumentException() public void Constructor_WithValidBookmark_ShouldCreateWatcher() { // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); reader.TryGetEvents(out _, 1); @@ -197,20 +197,6 @@ 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() { @@ -265,6 +251,20 @@ 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,20 +343,6 @@ 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() { @@ -412,28 +398,29 @@ public void Enabled_WhenSetToFalseTwice_ShouldNotThrow() } [Fact] - public void Enabled_WhenSetToSameValue_ShouldNotThrow() + public void Enabled_WhenSetToFalse_ShouldUnsubscribe() { // Arrange using var watcher = new EventLogWatcher(Constants.ApplicationLogName); - watcher.Enabled = false; + watcher.Enabled = true; - // Act & Assert + // Act watcher.Enabled = false; + + // Assert Assert.False(watcher.Enabled); } [Fact] - public void Enabled_WhenSetToTrue_ShouldSubscribe() + public void Enabled_WhenSetToSameValue_ShouldNotThrow() { // Arrange using var watcher = new EventLogWatcher(Constants.ApplicationLogName); + watcher.Enabled = false; - // Act - watcher.Enabled = true; - - // Assert - Assert.True(watcher.Enabled); + // Act & Assert + watcher.Enabled = false; + Assert.False(watcher.Enabled); } [Fact] @@ -475,6 +462,19 @@ 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() { @@ -988,7 +988,7 @@ public void EventRecordWritten_WhenSubscribed_ShouldReceiveEvents() public void EventRecordWritten_WithBookmark_ShouldReceiveNewEvents() { // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + using var reader = new EventLogReader(Constants.ApplicationLogName, LogPathType.Channel); reader.TryGetEvents(out _, 1); var bookmark = reader.LastBookmark; diff --git a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/SmallEvtxFixture.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/SmallEvtxFixture.cs index 340ad394..625baa53 100644 --- a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/SmallEvtxFixture.cs +++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/SmallEvtxFixture.cs @@ -1,6 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.Eventing.Readers; using System.Diagnostics; using System.Text.RegularExpressions; @@ -129,7 +130,7 @@ private void ExportRecordWindow(string wevtutilPath, long oldestId, long latestI private void VerifyMinimumEvents() { // Count records so an empty/gappy export produces a clear fixture-level error instead of cryptic test failures. - using var reader = new EventLogReader(FilePath, PathType.FilePath); + using var reader = new EventLogReader(FilePath, LogPathType.File); int total = 0; diff --git a/tests/Unit/EventLogExpert.EventDbTool.Tests/DiffDatabaseCommandTests.cs b/tests/Unit/EventLogExpert.EventDbTool.Tests/DiffDatabaseCommandTests.cs index 058e5f5c..05c8efab 100644 --- a/tests/Unit/EventLogExpert.EventDbTool.Tests/DiffDatabaseCommandTests.cs +++ b/tests/Unit/EventLogExpert.EventDbTool.Tests/DiffDatabaseCommandTests.cs @@ -3,8 +3,8 @@ using EventLogExpert.EventDbTool.Tests.TestUtils; using EventLogExpert.EventDbTool.Tests.TestUtils.Constants; -using EventLogExpert.Eventing.EventProviderDatabase; using EventLogExpert.Eventing.Logging; +using EventLogExpert.Eventing.ProviderDatabase; using NSubstitute; namespace EventLogExpert.EventDbTool.Tests; @@ -41,7 +41,7 @@ public void DiffDatabase_PreservesResolvedFromOwningPublisher() // Assert Assert.True(File.Exists(output), "Diff should have created the output database."); - using var verify = new EventProviderDbContext(output, true); + using var verify = new ProviderDbContext(output, true); var rows = verify.ProviderDetails.ToList(); var copied = Assert.Single(rows); Assert.Equal(Constants.SecondProviderName, copied.ProviderName); diff --git a/tests/Unit/EventLogExpert.EventDbTool.Tests/TestUtils/DatabaseTestUtils.cs b/tests/Unit/EventLogExpert.EventDbTool.Tests/TestUtils/DatabaseTestUtils.cs index e34975fa..fa8dbed6 100644 --- a/tests/Unit/EventLogExpert.EventDbTool.Tests/TestUtils/DatabaseTestUtils.cs +++ b/tests/Unit/EventLogExpert.EventDbTool.Tests/TestUtils/DatabaseTestUtils.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventProviderDatabase; +using EventLogExpert.Eventing.ProviderDatabase; using EventLogExpert.Eventing.Providers; using Microsoft.Data.Sqlite; @@ -67,7 +67,7 @@ public static void CreateV3Database(string dbPath) public static void CreateV4Database(string dbPath, params ProviderDetails[] providers) { - using var context = new EventProviderDbContext(dbPath, false); + using var context = new ProviderDbContext(dbPath, false); foreach (var provider in providers) { diff --git a/tests/Unit/EventLogExpert.EventDbTool.Tests/UpgradeDatabaseCommandTests.cs b/tests/Unit/EventLogExpert.EventDbTool.Tests/UpgradeDatabaseCommandTests.cs index 10d4c42b..ee375cf9 100644 --- a/tests/Unit/EventLogExpert.EventDbTool.Tests/UpgradeDatabaseCommandTests.cs +++ b/tests/Unit/EventLogExpert.EventDbTool.Tests/UpgradeDatabaseCommandTests.cs @@ -2,8 +2,8 @@ // // Licensed under the MIT License. using EventLogExpert.EventDbTool.Tests.TestUtils; -using EventLogExpert.Eventing.EventProviderDatabase; using EventLogExpert.Eventing.Logging; +using EventLogExpert.Eventing.ProviderDatabase; using NSubstitute; namespace EventLogExpert.EventDbTool.Tests; @@ -45,7 +45,7 @@ public void UpgradeDatabase_WithEmptyFile_LogsUnrecognizedSchemaWithoutCreatingT logger.Received().Error(Arg.Is(handler => handler.ToString().Contains("unrecognized schema") && handler.ToString().Contains(dbPath))); - using var verify = new EventProviderDbContext(dbPath, readOnly: true, ensureCreated: false); + using var verify = new ProviderDbContext(dbPath, readOnly: true, ensureCreated: false); var state = verify.IsUpgradeNeeded(); Assert.Equal(ProviderDatabaseSchemaVersion.Unknown, state.CurrentVersion); } @@ -84,7 +84,7 @@ public void UpgradeDatabase_WithV3Database_UpgradesToCurrentSchema() new UpgradeDatabaseCommand(logger).UpgradeDatabase(dbPath); - using var verify = new EventProviderDbContext(dbPath, true); + using var verify = new ProviderDbContext(dbPath, true); var schemaState = verify.IsUpgradeNeeded(); Assert.False(schemaState.NeedsUpgrade); Assert.Equal(ProviderDatabaseSchemaVersion.Current, schemaState.CurrentVersion); @@ -99,7 +99,7 @@ public void UpgradeDatabase_WithV4Database_LogsNoUpgradeNeeded() new UpgradeDatabaseCommand(logger).UpgradeDatabase(dbPath); - logger.Received().Info(Arg.Is(handler => + logger.Received().Information(Arg.Is(handler => handler.ToString().Contains("does not need to be upgraded"))); } diff --git a/tests/Unit/EventLogExpert.UI.Tests/LogNameMethodsTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Common/Channels/LogChannelMethodsTests.cs similarity index 60% rename from tests/Unit/EventLogExpert.UI.Tests/LogNameMethodsTests.cs rename to tests/Unit/EventLogExpert.Eventing.Tests/Common/Channels/LogChannelMethodsTests.cs index b46eaf0f..94188fda 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/LogNameMethodsTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/Common/Channels/LogChannelMethodsTests.cs @@ -1,16 +1,16 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI; +using EventLogExpert.Eventing.Common.Channels; -namespace EventLogExpert.UI.Tests; +namespace EventLogExpert.Eventing.Tests.Common.Channels; -public sealed class LogNameMethodsTests +public sealed class LogChannelMethodsTests { [Fact] public void GetMenuPath_WhenChannelHasEmptySegments_ShouldIgnoreEmpties() { - var path = LogNameMethods.GetMenuPath("Provider//Operational"); + var path = LogChannelMethods.GetMenuPath("Provider//Operational"); Assert.Equal(["Provider", "Operational"], path); } @@ -18,7 +18,7 @@ public void GetMenuPath_WhenChannelHasEmptySegments_ShouldIgnoreEmpties() [Fact] public void GetMenuPath_WhenChannelHasNestedSlashes_ShouldExpandEachSubChannel() { - var path = LogNameMethods.GetMenuPath("Microsoft-Windows-Foo/Bar/Baz"); + var path = LogChannelMethods.GetMenuPath("Microsoft-Windows-Foo/Bar/Baz"); Assert.Equal(["Microsoft", "Windows", "Foo", "Bar", "Baz"], path); } @@ -26,32 +26,32 @@ public void GetMenuPath_WhenChannelHasNestedSlashes_ShouldExpandEachSubChannel() [Fact] public void GetMenuPath_WhenInputIsNullOrWhitespace_ShouldReturnEmpty() { - Assert.Empty(LogNameMethods.GetMenuPath(string.Empty)); - Assert.Empty(LogNameMethods.GetMenuPath(" ")); + Assert.Empty(LogChannelMethods.GetMenuPath(string.Empty)); + Assert.Empty(LogChannelMethods.GetMenuPath(" ")); } [Fact] public void GetMenuPath_WhenLogNameContainsSpaces_ShouldKeepSegmentIntact() { - var path = LogNameMethods.GetMenuPath("Windows PowerShell"); + var path = LogChannelMethods.GetMenuPath("Windows PowerShell"); Assert.Equal(["Windows PowerShell"], path); } [Fact] - public void GetMenuPath_WhenMicrosoftWindowsChannel_ShouldExpandToFourSegments() + public void GetMenuPath_WhenMicrosoftWindowsChannelHasHyphen_ShouldKeepChannelIntact() { - var path = LogNameMethods.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] - public void GetMenuPath_WhenMicrosoftWindowsChannelHasHyphen_ShouldKeepChannelIntact() + public void GetMenuPath_WhenMicrosoftWindowsChannel_ShouldExpandToFourSegments() { - var path = LogNameMethods.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] @@ -59,7 +59,7 @@ public void GetMenuPath_WhenMicrosoftWindowsHasNoRemainder_ShouldFallBackToProvi { // Defensive: a malformed "Microsoft-Windows-/Operational" should not produce an empty // folder label. Falls through to the non-prefix branch and keeps the literal provider. - var path = LogNameMethods.GetMenuPath("Microsoft-Windows-/Operational"); + var path = LogChannelMethods.GetMenuPath("Microsoft-Windows-/Operational"); Assert.Equal(["Microsoft-Windows-", "Operational"], path); } @@ -67,7 +67,7 @@ public void GetMenuPath_WhenMicrosoftWindowsHasNoRemainder_ShouldFallBackToProvi [Fact] public void GetMenuPath_WhenMicrosoftWindowsProviderHasHyphen_ShouldKeepProviderIntact() { - var path = LogNameMethods.GetMenuPath("Microsoft-Windows-Kernel-Power/Operational"); + var path = LogChannelMethods.GetMenuPath("Microsoft-Windows-Kernel-Power/Operational"); Assert.Equal(["Microsoft", "Windows", "Kernel-Power", "Operational"], path); } @@ -75,7 +75,7 @@ public void GetMenuPath_WhenMicrosoftWindowsProviderHasHyphen_ShouldKeepProvider [Fact] public void GetMenuPath_WhenMicrosoftWindowsProviderHasMultipleHyphens_ShouldKeepProviderIntact() { - var path = LogNameMethods.GetMenuPath("Microsoft-Windows-AppXDeployment-Server/Operational"); + var path = LogChannelMethods.GetMenuPath("Microsoft-Windows-AppXDeployment-Server/Operational"); Assert.Equal(["Microsoft", "Windows", "AppXDeployment-Server", "Operational"], path); } @@ -83,7 +83,7 @@ public void GetMenuPath_WhenMicrosoftWindowsProviderHasMultipleHyphens_ShouldKee [Fact] public void GetMenuPath_WhenNonMicrosoftProviderHasHyphens_ShouldNotSplitOnHyphen() { - var path = LogNameMethods.GetMenuPath("Microsoft-Client-Licensing-Platform/Admin"); + var path = LogChannelMethods.GetMenuPath("Microsoft-Client-Licensing-Platform/Admin"); Assert.Equal(["Microsoft-Client-Licensing-Platform", "Admin"], path); } @@ -91,7 +91,7 @@ public void GetMenuPath_WhenNonMicrosoftProviderHasHyphens_ShouldNotSplitOnHyphe [Fact] public void GetMenuPath_WhenPrefixCasingDiffers_ShouldStillRecognizeMicrosoftWindows() { - var path = LogNameMethods.GetMenuPath("microsoft-windows-AAD/Operational"); + var path = LogChannelMethods.GetMenuPath("microsoft-windows-AAD/Operational"); Assert.Equal(["Microsoft", "Windows", "AAD", "Operational"], path); } @@ -99,7 +99,7 @@ public void GetMenuPath_WhenPrefixCasingDiffers_ShouldStillRecognizeMicrosoftWin [Fact] public void GetMenuPath_WhenProviderEndsWithSlash_ShouldOmitTrailingEmptyChannel() { - var path = LogNameMethods.GetMenuPath("Provider/"); + var path = LogChannelMethods.GetMenuPath("Provider/"); Assert.Equal(["Provider"], path); } @@ -107,7 +107,7 @@ public void GetMenuPath_WhenProviderEndsWithSlash_ShouldOmitTrailingEmptyChannel [Fact] public void GetMenuPath_WhenSimpleProviderWithChannel_ShouldSplitOnSlashOnly() { - var path = LogNameMethods.GetMenuPath("OpenSSH/Operational"); + var path = LogChannelMethods.GetMenuPath("OpenSSH/Operational"); Assert.Equal(["OpenSSH", "Operational"], path); } @@ -115,26 +115,26 @@ public void GetMenuPath_WhenSimpleProviderWithChannel_ShouldSplitOnSlashOnly() [Fact] public void GetMenuPath_WhenSimpleRootLog_ShouldReturnSingleSegment() { - var path = LogNameMethods.GetMenuPath("Application"); + var path = LogChannelMethods.GetMenuPath("Application"); Assert.Equal(["Application"], path); } [Fact] - public void HardCodedLiveLogNames_ContainsExpectedNames() + public void HardCodedLiveChannels_ContainsExpectedNames() { - Assert.Contains("Application", LogNameMethods.HardCodedLiveLogNames); - Assert.Contains("System", LogNameMethods.HardCodedLiveLogNames); - Assert.Contains("Security", LogNameMethods.HardCodedLiveLogNames); - Assert.Equal(3, LogNameMethods.HardCodedLiveLogNames.Count); + Assert.Contains("Application", LogChannelMethods.HardCodedLiveChannels); + Assert.Contains("System", LogChannelMethods.HardCodedLiveChannels); + Assert.Contains("Security", LogChannelMethods.HardCodedLiveChannels); + Assert.Equal(3, LogChannelMethods.HardCodedLiveChannels.Count); } [Theory] [InlineData("application")] [InlineData("APPLICATION")] [InlineData("Application")] - public void HardCodedLiveLogNames_ShouldMatchCaseInsensitively(string input) + public void HardCodedLiveChannels_ShouldMatchCaseInsensitively(string input) { - Assert.Contains(input, LogNameMethods.HardCodedLiveLogNames); + Assert.Contains(input, LogChannelMethods.HardCodedLiveChannels); } } diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Common/Channels/LogChannelNamesTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Common/Channels/LogChannelNamesTests.cs new file mode 100644 index 00000000..6993a7aa --- /dev/null +++ b/tests/Unit/EventLogExpert.Eventing.Tests/Common/Channels/LogChannelNamesTests.cs @@ -0,0 +1,36 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Channels; + +namespace EventLogExpert.Eventing.Tests.Common.Channels; + +public sealed class LogChannelNamesTests +{ + [Fact] + public void AdminOnlyLiveChannels_ContainsExpectedNames() + { + Assert.Contains(LogChannelNames.SecurityLog, LogChannelNames.AdminOnlyLiveChannels); + Assert.Contains(LogChannelNames.StateLog, LogChannelNames.AdminOnlyLiveChannels); + Assert.Equal(2, LogChannelNames.AdminOnlyLiveChannels.Count); + } + + [Theory] + [InlineData("security")] + [InlineData("SECURITY")] + [InlineData("state")] + [InlineData("STATE")] + public void AdminOnlyLiveChannels_ShouldMatchCaseInsensitively(string input) + { + Assert.Contains(input, LogChannelNames.AdminOnlyLiveChannels); + } + + [Fact] + public void Constants_HaveExpectedValues() + { + Assert.Equal("Application", LogChannelNames.ApplicationLog); + Assert.Equal("Security", LogChannelNames.SecurityLog); + Assert.Equal("State", LogChannelNames.StateLog); + Assert.Equal("System", LogChannelNames.SystemLog); + } +} diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Common/Databases/DatabasePathSorterTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Common/Databases/DatabasePathSorterTests.cs new file mode 100644 index 00000000..0f2192fc --- /dev/null +++ b/tests/Unit/EventLogExpert.Eventing.Tests/Common/Databases/DatabasePathSorterTests.cs @@ -0,0 +1,151 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Databases; + +namespace EventLogExpert.Eventing.Tests.Common.Databases; + +public sealed class DatabasePathSorterTests +{ + [Fact] + public void Sort_PreservesDirectory_ShouldReturnFullPaths() + { + var databases = new[] + { + @"C:\Databases\Test\Windows 2019.db", + @"C:\Databases\Prod\Exchange 2016.db" + }; + + var sorted = DatabasePathSorter.Sort(databases); + + Assert.All(sorted, path => Assert.True(Path.IsPathRooted(path))); + Assert.Contains(sorted, s => s.Contains(@"Databases\Test")); + Assert.Contains(sorted, s => s.Contains(@"Databases\Prod")); + } + + [Fact] + public void Sort_WithComplexVersionStrings_ShouldHandleCorrectly() + { + var databases = new[] + { + @"C:\Test\Product v2.0.db", + @"C:\Test\Product v1.5.db", + @"C:\Test\Product RC1.db" + }; + + var sorted = DatabasePathSorter.Sort(databases); + + Assert.Equal(3, sorted.Count); + Assert.Equal(@"C:\Test\Product v2.0.db", sorted[0]); + Assert.Equal(@"C:\Test\Product v1.5.db", sorted[1]); + Assert.Equal(@"C:\Test\Product RC1.db", sorted[2]); + } + + [Fact] + public void Sort_WithEmptyList_ShouldReturnEmptyList() + { + var sorted = DatabasePathSorter.Sort([]); + + Assert.Empty(sorted); + } + + [Fact] + public void Sort_WithMixedVersionsAndNoVersions_ShouldSortCorrectly() + { + var databases = new[] + { + @"C:\Test\Windows 2019.db", + @"C:\Test\Exchange.db", + @"C:\Test\Windows 2016.db", + @"C:\Test\Azure 2020.db" + }; + + var sorted = DatabasePathSorter.Sort(databases); + + Assert.Equal(4, sorted.Count); + Assert.Equal(@"C:\Test\Azure 2020.db", sorted[0]); + Assert.Equal(@"C:\Test\Exchange.db", sorted[1]); + Assert.Equal(@"C:\Test\Windows 2019.db", sorted[2]); + Assert.Equal(@"C:\Test\Windows 2016.db", sorted[3]); + } + + [Fact] + public void Sort_WithNoVersion_ShouldSortByProductName() + { + var databases = new[] + { + @"C:\Test\Windows.db", + @"C:\Test\Exchange.db", + @"C:\Test\Azure.db" + }; + + var sorted = DatabasePathSorter.Sort(databases); + + Assert.Equal(3, sorted.Count); + Assert.Equal(@"C:\Test\Azure.db", sorted[0]); + Assert.Equal(@"C:\Test\Exchange.db", sorted[1]); + Assert.Equal(@"C:\Test\Windows.db", sorted[2]); + } + + [Fact] + public void Sort_WithNumericVersions_ShouldSortDescending() + { + var databases = new[] + { + @"C:\Test\Product 10.db", + @"C:\Test\Product 2.db", + @"C:\Test\Product 20.db" + }; + + var sorted = DatabasePathSorter.Sort(databases); + + Assert.Equal(3, sorted.Count); + Assert.Equal(@"C:\Test\Product 20.db", sorted[0]); + Assert.Equal(@"C:\Test\Product 10.db", sorted[1]); + Assert.Equal(@"C:\Test\Product 2.db", sorted[2]); + } + + [Fact] + public void Sort_WithProductNameAndVersion_ShouldSortByProductAscendingVersionDescending() + { + var databases = new[] + { + @"C:\Test\Windows 2016.db", + @"C:\Test\Windows 2019.db", + @"C:\Test\Exchange 2019.db", + @"C:\Test\Exchange 2016.db" + }; + + var sorted = DatabasePathSorter.Sort(databases); + + Assert.Equal(4, sorted.Count); + Assert.Equal(@"C:\Test\Exchange 2019.db", sorted[0]); + Assert.Equal(@"C:\Test\Exchange 2016.db", sorted[1]); + Assert.Equal(@"C:\Test\Windows 2019.db", sorted[2]); + Assert.Equal(@"C:\Test\Windows 2016.db", sorted[3]); + } + + [Fact] + public void Sort_WithSingleDatabase_ShouldReturnSameDatabase() + { + var databases = new[] { @"C:\Test\Windows 2019.db" }; + + var sorted = DatabasePathSorter.Sort(databases); + + Assert.Single(sorted); + Assert.Equal(@"C:\Test\Windows 2019.db", sorted[0]); + } + + [Fact] + public void Sort_WithFileNamesOnly_PreservesNames() + { + var fileNames = new[] { "Exchange 2019.db", "Exchange 2016.db", "Windows 2019.db" }; + + var sorted = DatabasePathSorter.Sort(fileNames); + + Assert.Equal(3, sorted.Count); + Assert.Equal("Exchange 2019.db", sorted[0]); + Assert.Equal("Exchange 2016.db", sorted[1]); + Assert.Equal("Windows 2019.db", sorted[2]); + } +} diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/EventProviderDatabase/CompressedJsonValueConverterTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/CompressedJsonValueConverterTests.cs similarity index 98% rename from tests/Unit/EventLogExpert.Eventing.Tests/EventProviderDatabase/CompressedJsonValueConverterTests.cs rename to tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/CompressedJsonValueConverterTests.cs index 20ff28ce..79e0f32d 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/EventProviderDatabase/CompressedJsonValueConverterTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/CompressedJsonValueConverterTests.cs @@ -1,11 +1,11 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventProviderDatabase; +using EventLogExpert.Eventing.ProviderDatabase; using EventLogExpert.Eventing.Tests.TestUtils; using System.Text.Json; -namespace EventLogExpert.Eventing.Tests.EventProviderDatabase; +namespace EventLogExpert.Eventing.Tests.ProviderDatabase; public sealed class CompressedJsonValueConverterTests { diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/EventProviderDatabase/EventProviderDbContextTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderDbContextTests.cs similarity index 90% rename from tests/Unit/EventLogExpert.Eventing.Tests/EventProviderDatabase/EventProviderDbContextTests.cs rename to tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderDbContextTests.cs index e2266cad..9aac8bff 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/EventProviderDatabase/EventProviderDbContextTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderDbContextTests.cs @@ -1,18 +1,17 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventProviderDatabase; using EventLogExpert.Eventing.Logging; -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.ProviderDatabase; using EventLogExpert.Eventing.Providers; using EventLogExpert.Eventing.Tests.TestUtils; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using NSubstitute; -namespace EventLogExpert.Eventing.Tests.EventProviderDatabase; +namespace EventLogExpert.Eventing.Tests.ProviderDatabase; -public sealed class EventProviderDbContextTests : IDisposable +public sealed class ProviderDbContextTests : IDisposable { private readonly List _tempDatabases = []; @@ -23,7 +22,7 @@ public void Constructor_ShouldCreateNewDatabase() var dbPath = CreateTempDatabasePath(); // Act - using var context = new EventProviderDbContext(dbPath, false); + using var context = new ProviderDbContext(dbPath, false); // Assert Assert.True(File.Exists(dbPath)); @@ -40,7 +39,7 @@ public void Constructor_WithEnsureCreatedFalse_DoesNotCreateDatabase() var dbPath = CreateTempDatabasePath(); // Act - using (var context = new EventProviderDbContext(dbPath, readOnly: false, ensureCreated: false)) + using (var context = new ProviderDbContext(dbPath, readOnly: false, ensureCreated: false)) { // Touching the connection forces SQLite to open the file so the assertion below // exercises the same on-disk state callers would observe. @@ -66,10 +65,10 @@ public void Constructor_WithLogger_ShouldLogInstantiation() var logger = Substitute.For(); // Act - using var context = new EventProviderDbContext(dbPath, false, logger); + using var context = new ProviderDbContext(dbPath, false, logger); // Assert - logger.Received(1).Debug(Arg.Is(h => h.ToString().Contains("Instantiating EventProviderDbContext"))); + logger.Received(1).Debug(Arg.Is(h => h.ToString().Contains("Instantiating ProviderDbContext"))); } [Fact] @@ -78,13 +77,13 @@ public void Constructor_WithReadOnly_ShouldOpenDatabaseInReadOnlyMode() // Arrange var dbPath = CreateTempDatabasePath(); - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.SaveChanges(); } // Act - using var readOnlyContext = new EventProviderDbContext(dbPath, true); + using var readOnlyContext = new ProviderDbContext(dbPath, true); // Assert Assert.NotNull(readOnlyContext.ProviderDetails); @@ -104,7 +103,7 @@ public void Database_ShouldSupportConcurrentReads() // Arrange var dbPath = CreateTempDatabasePath(); - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { for (int i = 0; i < 10; i++) { @@ -122,7 +121,7 @@ public void Database_ShouldSupportConcurrentReads() { try { - using var context = new EventProviderDbContext(dbPath, true); + using var context = new ProviderDbContext(dbPath, true); results[i] = context.ProviderDetails.Count(); } catch (Exception ex) @@ -142,7 +141,7 @@ public void Detection_FreshDatabase_ReportsV4() // Arrange var dbPath = CreateTempDatabasePath(); - using var context = new EventProviderDbContext(dbPath, false); + using var context = new ProviderDbContext(dbPath, false); // Act var state = context.IsUpgradeNeeded(); @@ -160,7 +159,7 @@ public void Detection_V3Schema_NeedsV4Upgrade() SeedV3Schema(dbPath); // Act - using var context = new EventProviderDbContext(dbPath, false); + using var context = new ProviderDbContext(dbPath, false); var state = context.IsUpgradeNeeded(); // Assert @@ -183,14 +182,14 @@ public void IsUpgradeNeeded_ShouldLogResult() var dbPath = CreateTempDatabasePath(); var logger = Substitute.For(); - using var context = new EventProviderDbContext(dbPath, false, logger); + using var context = new ProviderDbContext(dbPath, false, logger); // Act var result = context.IsUpgradeNeeded(); // Assert logger.Received(1).Debug(Arg.Is(h => - h.ToString().Contains(nameof(EventProviderDbContext.IsUpgradeNeeded)) && + h.ToString().Contains(nameof(ProviderDbContext.IsUpgradeNeeded)) && h.ToString().Contains("currentVersion") && h.ToString().Contains("needsUpgrade"))); } @@ -219,7 +218,7 @@ public void IsUpgradeNeeded_WithMixedPayloadColumnTypes_ReportsUnknownSentinel() } // Act - using var context = new EventProviderDbContext(dbPath, false); + using var context = new ProviderDbContext(dbPath, false); var state = context.IsUpgradeNeeded(); // Assert @@ -233,7 +232,7 @@ public void IsUpgradeNeeded_WithNewDatabase_ShouldReportCurrentSchemaAndNoUpgrad // Arrange var dbPath = CreateTempDatabasePath(); - using var context = new EventProviderDbContext(dbPath, false); + using var context = new ProviderDbContext(dbPath, false); // Act var state = context.IsUpgradeNeeded(); @@ -267,7 +266,7 @@ public void IsUpgradeNeeded_WithUnknownShape_ReportsUnknownSentinel() } // Act - using var context = new EventProviderDbContext(dbPath, false); + using var context = new ProviderDbContext(dbPath, false); var state = context.IsUpgradeNeeded(); // Assert — Unknown sentinel is reported (NeedsUpgrade is true so callers route through @@ -284,7 +283,7 @@ public void IsUpgradeNeeded_WithV1Schema_ShouldReportV1AndUpgradeNeeded() SeedLegacySchema(dbPath, includeParameters: false, parametersType: null, messagesType: "TEXT"); // Act - using var context = new EventProviderDbContext(dbPath, false); + using var context = new ProviderDbContext(dbPath, false); var state = context.IsUpgradeNeeded(); // Assert @@ -300,7 +299,7 @@ public void IsUpgradeNeeded_WithV2Schema_ShouldReportV2AndUpgradeNeeded() SeedLegacySchema(dbPath, includeParameters: true, parametersType: "TEXT", messagesType: "TEXT"); // Act - using var context = new EventProviderDbContext(dbPath, false); + using var context = new ProviderDbContext(dbPath, false); var state = context.IsUpgradeNeeded(); // Assert @@ -317,7 +316,7 @@ public void Name_ShouldNotIncludeFileExtension() _tempDatabases.Add(dbPath); // Act - using var context = new EventProviderDbContext(dbPath, false); + using var context = new ProviderDbContext(dbPath, false); // Assert Assert.Equal("Database.With.Multiple.Dots", context.Name); @@ -329,7 +328,7 @@ public void PerformUpgradeIfNeeded_WithNewDatabase_ShouldDoNothing() { // Arrange var dbPath = CreateTempDatabasePath(); - using var context = new EventProviderDbContext(dbPath, false); + using var context = new ProviderDbContext(dbPath, false); var initialSize = new FileInfo(dbPath).Length; // Act @@ -368,7 +367,7 @@ public void PerformUpgradeIfNeeded_WithUnknownShape_ThrowsAndPreservesTable() // Act + Assert — distinct error message, file untouched. DatabaseUpgradeException? thrown; - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { thrown = Assert.Throws(() => context.PerformUpgradeIfNeeded()); } @@ -397,7 +396,7 @@ public void PerformUpgradeIfNeeded_WithV1Schema_ThrowsAndPreservesLegacyTable() // Act + Assert — V1 is no longer auto-upgradable; the upgrade fails fast. DatabaseUpgradeException? thrown; - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { thrown = Assert.Throws(() => context.PerformUpgradeIfNeeded()); } @@ -407,7 +406,7 @@ public void PerformUpgradeIfNeeded_WithV1Schema_ThrowsAndPreservesLegacyTable() Assert.Equal(dbPath, thrown.DatabasePath); // The original V1 table is preserved (throw fires before DROP); detection still reports v1. - using var verify = new EventProviderDbContext(dbPath, true); + using var verify = new ProviderDbContext(dbPath, true); var stateAfter = verify.IsUpgradeNeeded(); Assert.Equal(1, stateAfter.CurrentVersion); Assert.True(stateAfter.NeedsUpgrade); @@ -415,60 +414,60 @@ public void PerformUpgradeIfNeeded_WithV1Schema_ThrowsAndPreservesLegacyTable() } [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 EventProviderDbContext(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 EventProviderDbContext(dbPath, true); - var stateAfter = verify.IsUpgradeNeeded(); - Assert.Equal(2, stateAfter.CurrentVersion); AssertProviderDetailsRowCount(dbPath, expectedRows: 1); } [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 EventProviderDbContext(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); } @@ -479,7 +478,7 @@ public void ProviderDetails_Delete_ShouldRemoveRecord() var dbPath = CreateTempDatabasePath(); var providerName = "DeletableProvider"; - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.ProviderDetails.Add(EventUtils.CreateProvider(providerName)); @@ -487,7 +486,7 @@ public void ProviderDetails_Delete_ShouldRemoveRecord() } // Act - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { var provider = context.ProviderDetails.First(p => p.ProviderName == providerName); context.ProviderDetails.Remove(provider); @@ -495,7 +494,7 @@ public void ProviderDetails_Delete_ShouldRemoveRecord() } // Assert - using (var context = new EventProviderDbContext(dbPath, true)) + using (var context = new ProviderDbContext(dbPath, true)) { Assert.Empty(context.ProviderDetails.Where(p => p.ProviderName == providerName)); } @@ -508,7 +507,7 @@ public void ProviderDetails_ShouldBeAccessible() var dbPath = CreateTempDatabasePath(); // Act - using var context = new EventProviderDbContext(dbPath, false); + using var context = new ProviderDbContext(dbPath, false); // Assert Assert.NotNull(context.ProviderDetails); @@ -531,7 +530,7 @@ public void ProviderDetails_ShouldCompressLargeData() const int UncompressedDataSize = 100 * 1000; // Act - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.ProviderDetails.Add(provider); context.SaveChanges(); @@ -546,7 +545,7 @@ public void ProviderDetails_ShouldCompressLargeData() $"Expected compressed file size to be less than {UncompressedDataSize / 2} bytes, but was {fileSize} bytes. " + $"This suggests compression may not be working effectively."); - using (var context = new EventProviderDbContext(dbPath, true)) + using (var context = new ProviderDbContext(dbPath, true)) { var retrieved = context.ProviderDetails.FirstOrDefault(p => p.ProviderName == "LargeProvider"); Assert.NotNull(retrieved); @@ -561,7 +560,7 @@ public void ProviderDetails_Update_ShouldModifyExistingRecord() var dbPath = CreateTempDatabasePath(); var providerName = "UpdateableProvider"; - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.ProviderDetails.Add(EventUtils.CreateProvider( providerName, @@ -571,7 +570,7 @@ public void ProviderDetails_Update_ShouldModifyExistingRecord() } // Act - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { var provider = context.ProviderDetails.First(p => p.ProviderName == providerName); provider.Messages = [EventUtils.CreateMessageModel(providerName, 1, "Updated")]; @@ -579,7 +578,7 @@ public void ProviderDetails_Update_ShouldModifyExistingRecord() } // Assert - using (var context = new EventProviderDbContext(dbPath, true)) + using (var context = new ProviderDbContext(dbPath, true)) { var provider = context.ProviderDetails.First(p => p.ProviderName == providerName); Assert.Equal("Updated", provider.Messages.First().Text); @@ -631,14 +630,14 @@ public void ProviderDetails_WithComplexData_ShouldPreserveAllFields() }; // Act - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.ProviderDetails.Add(provider); context.SaveChanges(); } // Assert - using (var context = new EventProviderDbContext(dbPath, true)) + using (var context = new ProviderDbContext(dbPath, true)) { var retrieved = context.ProviderDetails.First(p => p.ProviderName == "ComplexProvider"); Assert.Equal(3, retrieved.Messages.Count()); @@ -665,14 +664,14 @@ public void ProviderDetails_WithEmptyCollections_ShouldPersist() var provider = EventUtils.CreateProvider("EmptyProvider"); // Act - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.ProviderDetails.Add(provider); context.SaveChanges(); } // Assert - using (var context = new EventProviderDbContext(dbPath, true)) + using (var context = new ProviderDbContext(dbPath, true)) { var retrieved = context.ProviderDetails.First(p => p.ProviderName == "EmptyProvider"); Assert.NotNull(retrieved); @@ -696,7 +695,7 @@ public void ProviderDetails_WithMultipleProviders_ShouldRetrieveAll() .ToList(); // Act - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { foreach (var provider in providers) { @@ -707,7 +706,7 @@ public void ProviderDetails_WithMultipleProviders_ShouldRetrieveAll() } // Assert - using (var context = new EventProviderDbContext(dbPath, true)) + using (var context = new ProviderDbContext(dbPath, true)) { Assert.Equal(5, context.ProviderDetails.Count()); } @@ -724,14 +723,14 @@ public void ProviderDetails_WithSpecialCharacters_ShouldPersist() messages: [EventUtils.CreateMessageModel("Special\"Provider'With<>Chars", 1, "Message with \"quotes\" and 'apostrophes'")]); // Act - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.ProviderDetails.Add(provider); context.SaveChanges(); } // Assert - using (var context = new EventProviderDbContext(dbPath, true)) + using (var context = new ProviderDbContext(dbPath, true)) { var retrieved = context.ProviderDetails.First(p => p.ProviderName == "Special\"Provider'With<>Chars"); Assert.NotNull(retrieved); @@ -745,7 +744,7 @@ public void ProviderName_LookupOnV4Schema_UsesPrimaryKeyIndex() // Arrange — populate a V4 database so EXPLAIN QUERY PLAN has rows to plan against. var dbPath = CreateTempDatabasePath(); - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { for (var i = 0; i < 5; i++) { @@ -794,14 +793,14 @@ public void ResolvedFromOwningPublisher_RoundTripsThroughDatabase() resolvedFromOwningPublisher: "Owning-Publisher-Name"); // Act - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.ProviderDetails.Add(provider); context.SaveChanges(); } // Assert - using var verify = new EventProviderDbContext(dbPath, true); + using var verify = new ProviderDbContext(dbPath, true); var retrieved = verify.ProviderDetails.Single(p => p.ProviderName == "ResolvedRoundTrip"); Assert.Equal("Owning-Publisher-Name", retrieved.ResolvedFromOwningPublisher); } @@ -812,7 +811,7 @@ public void Schema_V4_PrimaryKey_UsesNoCaseCollation() // Arrange — a fresh database is created at the current schema version. var dbPath = CreateTempDatabasePath(); - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.SaveChanges(); } @@ -838,7 +837,7 @@ public void Upgrade_V3_To_V4_HappyPath() InsertV3Row(dbPath, seeded); // Act - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.PerformUpgradeIfNeeded(); } @@ -846,7 +845,7 @@ public void Upgrade_V3_To_V4_HappyPath() // Assert Assert.Equal("NOCASE", ReadProviderNamePrimaryKeyCollation(dbPath), ignoreCase: true); - using var verify = new EventProviderDbContext(dbPath, true); + using var verify = new ProviderDbContext(dbPath, true); var stateAfter = verify.IsUpgradeNeeded(); Assert.Equal(ProviderDatabaseSchemaVersion.Current, stateAfter.CurrentVersion); Assert.False(stateAfter.NeedsUpgrade); @@ -882,13 +881,13 @@ public void Upgrade_V3_To_V4_MergesCaseCollidingProviders() InsertV3Row(dbPath, second); // Act - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.PerformUpgradeIfNeeded(); } // Assert — merged into a single row using the first-encountered casing as canonical. - using var verify = new EventProviderDbContext(dbPath, true); + using var verify = new ProviderDbContext(dbPath, true); var rows = verify.ProviderDetails.ToList(); Assert.Single(rows); Assert.Equal("Microsoft-Foo", rows[0].ProviderName); @@ -941,13 +940,13 @@ public void Upgrade_V3_To_V4_MergesCaseCollidingProvidersWithParameters() InsertV3Row(dbPath, second); // Act - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.PerformUpgradeIfNeeded(); } // Assert — single merged row using first-encountered casing; deduplicated parameters union. - using var verify = new EventProviderDbContext(dbPath, true); + using var verify = new ProviderDbContext(dbPath, true); var rows = verify.ProviderDetails.ToList(); Assert.Single(rows); Assert.Equal("Microsoft-Foo", rows[0].ProviderName); @@ -977,7 +976,7 @@ public void Upgrade_V3_To_V4_OnConflict_ThrowsAndPreservesOriginalRows() keywords: new Dictionary { { 1L, "second" } })); // Act + Assert — upgrade fails fast and the V3 table is not dropped. - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { Assert.Throws(() => context.PerformUpgradeIfNeeded()); } @@ -1016,13 +1015,13 @@ public void Upgrade_V3_To_V4_PreservesNonEmptyParameters() InsertV3Row(dbPath, seeded); // Act - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.PerformUpgradeIfNeeded(); } // Assert — both parameter rows survived the destructive upgrade. - using var verify = new EventProviderDbContext(dbPath, true); + using var verify = new ProviderDbContext(dbPath, true); var row = verify.ProviderDetails.Single(p => p.ProviderName == "V3-WithParams"); var parameters = row.Parameters.ToList(); Assert.Equal(2, parameters.Count); @@ -1189,7 +1188,7 @@ private static void SeedLegacySchema( string messagesType) { // Build a legacy ProviderDetails table directly via raw SQLite, before any - // EventProviderDbContext touches the file. EnsureCreated() will then be a no-op + // ProviderDbContext touches the file. EnsureCreated() will then be a no-op // because the table already exists, so the legacy schema reaches IsUpgradeNeeded // and PerformUpgradeIfNeeded unmodified. using var connection = new SqliteConnection($"Data Source={dbPath}"); @@ -1219,7 +1218,7 @@ private static void SeedV3Schema(string dbPath) { // V3 schema: BLOB payload columns, Parameters as BLOB, no ResolvedFromOwningPublisher // column, default (BINARY) collation on the ProviderName PK. Built by raw SQL so - // EventProviderDbContext.EnsureCreated() sees the table as already-existing and skips + // ProviderDbContext.EnsureCreated() sees the table as already-existing and skips // its V4 schema generation, exposing the legacy V3 shape to detection and upgrade paths. using var connection = new SqliteConnection($"Data Source={dbPath}"); connection.Open(); diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/EventProviderDatabase/ProviderDetailsMergerTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderDetailsMergerTests.cs similarity index 98% rename from tests/Unit/EventLogExpert.Eventing.Tests/EventProviderDatabase/ProviderDetailsMergerTests.cs rename to tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderDetailsMergerTests.cs index 8cd8fb33..dd0b8645 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/EventProviderDatabase/ProviderDetailsMergerTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderDetailsMergerTests.cs @@ -1,12 +1,11 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventProviderDatabase; -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.ProviderDatabase; using EventLogExpert.Eventing.Providers; using EventLogExpert.Eventing.Tests.TestUtils; -namespace EventLogExpert.Eventing.Tests.EventProviderDatabase; +namespace EventLogExpert.Eventing.Tests.ProviderDatabase; public sealed class ProviderDetailsMergerTests { diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/EventProviderDatabase/ProviderJsonContextTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderJsonContextTests.cs similarity index 94% rename from tests/Unit/EventLogExpert.Eventing.Tests/EventProviderDatabase/ProviderJsonContextTests.cs rename to tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderJsonContextTests.cs index 46c8a565..6bd7ac3a 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/EventProviderDatabase/ProviderJsonContextTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderJsonContextTests.cs @@ -1,10 +1,10 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventProviderDatabase; -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.ProviderDatabase; +using EventLogExpert.Eventing.Providers; -namespace EventLogExpert.Eventing.Tests.EventProviderDatabase; +namespace EventLogExpert.Eventing.Tests.ProviderDatabase; public sealed class ProviderJsonContextTests { @@ -33,6 +33,24 @@ public void Default_ShouldProvideMetadataFor_ProviderDtoType(Type type) Assert.Equal(type, typeInfo!.Type); } + [Fact] + public void RoundTrip_IDictionaryLongString_PreservesData() + { + IDictionary original = new Dictionary + { + [1L] = "one", + [0x100000000L] = "big" + }; + + var bytes = CompressedJsonValueConverter>.ConvertToCompressedJson(original); + var restored = CompressedJsonValueConverter>.ConvertFromCompressedJson(bytes); + + Assert.NotNull(restored); + Assert.Equal(2, restored.Count); + Assert.Equal("one", restored[1L]); + Assert.Equal("big", restored[0x100000000L]); + } + [Fact] public void RoundTrip_IReadOnlyListMessageModel_PreservesData() { @@ -62,22 +80,4 @@ public void RoundTrip_IReadOnlyListMessageModel_PreservesData() Assert.Equal(sample.Text, restored[0].Text); Assert.Equal(sample.LogLink, restored[0].LogLink); } - - [Fact] - public void RoundTrip_IDictionaryLongString_PreservesData() - { - IDictionary original = new Dictionary - { - [1L] = "one", - [0x100000000L] = "big" - }; - - var bytes = CompressedJsonValueConverter>.ConvertToCompressedJson(original); - var restored = CompressedJsonValueConverter>.ConvertFromCompressedJson(bytes); - - Assert.NotNull(restored); - Assert.Equal(2, restored.Count); - Assert.Equal("one", restored[1L]); - Assert.Equal("big", restored[0x100000000L]); - } } diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs index 970dc661..1637b07e 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs @@ -23,14 +23,14 @@ public void Constructor_WhenDifferentProviderNames_ShouldCreateInstances(string } [Fact] - public void GetMessages_WhenDuplicateFiles_ShouldProcessAll() + public void LoadMessagesFromFiles_WhenDuplicateFiles_ShouldProcessAll() { // Arrange var duplicateFiles = new[] { Constants.NonExistentDll, Constants.NonExistentDll }; var mockLogger = Substitute.For(); // Act - var messages = EventMessageProvider.GetMessages(duplicateFiles, Constants.TestProviderName, mockLogger); + var messages = EventMessageProvider.LoadMessagesFromFiles(duplicateFiles, Constants.TestProviderName, mockLogger); // Assert Assert.NotNull(messages); @@ -47,10 +47,10 @@ public void GetMessages_WhenDuplicateFiles_ShouldProcessAll() } [Fact] - public void GetMessages_WhenEmptyFileList_ShouldReturnEmptyList() + public void LoadMessagesFromFiles_WhenEmptyFileList_ShouldReturnEmptyList() { // Arrange & Act - var messages = EventMessageProvider.GetMessages([], Constants.TestProviderName); + var messages = EventMessageProvider.LoadMessagesFromFiles([], Constants.TestProviderName); // Assert Assert.NotNull(messages); @@ -58,38 +58,38 @@ public void GetMessages_WhenEmptyFileList_ShouldReturnEmptyList() } [Fact] - public void GetMessages_WhenFileListIsNull_ShouldThrowArgumentNullException() + public void LoadMessagesFromFiles_WhenFileListIsNull_ShouldThrowArgumentNullException() { // Arrange var mockLogger = Substitute.For(); // Act & Assert Assert.Throws(() => - EventMessageProvider.GetMessages(null!, Constants.TestProviderName, mockLogger)); + EventMessageProvider.LoadMessagesFromFiles(null!, Constants.TestProviderName, mockLogger)); } [Fact] - public void GetMessages_WhenFilePathHasMultipleBackslashes_ShouldExtractFileName() + public void LoadMessagesFromFiles_WhenFilePathHasMultipleBackslashes_ShouldExtractFileName() { // Arrange var filesWithPath = new[] { Constants.NonExistentDllFullPath }; // Act - var messages = EventMessageProvider.GetMessages(filesWithPath, Constants.TestProviderName); + var messages = EventMessageProvider.LoadMessagesFromFiles(filesWithPath, Constants.TestProviderName); // Assert Assert.NotNull(messages); } [Fact] - public void GetMessages_WhenInvalidFile_ShouldLogWarning() + public void LoadMessagesFromFiles_WhenInvalidFile_ShouldLogWarning() { // Arrange var invalidFiles = new[] { Constants.NonExistentDll }; var mockLogger = Substitute.For(); // Act - EventMessageProvider.GetMessages(invalidFiles, Constants.TestProviderName, mockLogger); + EventMessageProvider.LoadMessagesFromFiles(invalidFiles, Constants.TestProviderName, mockLogger); // Assert: an unresolvable path produces a LoadLibraryEx failure log for both the // MUI-aware primary attempt and the leaf-name fallback. @@ -105,13 +105,13 @@ public void GetMessages_WhenInvalidFile_ShouldLogWarning() } [Fact] - public void GetMessages_WhenInvalidFile_ShouldReturnEmptyList() + public void LoadMessagesFromFiles_WhenInvalidFile_ShouldReturnEmptyList() { // Arrange var invalidFiles = new[] { Constants.NonExistentDll }; // Act - var messages = EventMessageProvider.GetMessages(invalidFiles, Constants.TestProviderName); + var messages = EventMessageProvider.LoadMessagesFromFiles(invalidFiles, Constants.TestProviderName); // Assert Assert.NotNull(messages); @@ -119,14 +119,14 @@ public void GetMessages_WhenInvalidFile_ShouldReturnEmptyList() } [Fact] - public void GetMessages_WhenMultipleInvalidFiles_ShouldLogMultipleWarnings() + public void LoadMessagesFromFiles_WhenMultipleInvalidFiles_ShouldLogMultipleWarnings() { // Arrange var invalidFiles = new[] { Constants.NonExistentDll, Constants.NonExistentDll }; var mockLogger = Substitute.For(); // Act - EventMessageProvider.GetMessages(invalidFiles, Constants.TestProviderName, mockLogger); + 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 @@ -140,13 +140,13 @@ public void GetMessages_WhenMultipleInvalidFiles_ShouldLogMultipleWarnings() } [Fact] - public void GetMessages_WhenMultipleInvalidFiles_ShouldReturnEmptyList() + public void LoadMessagesFromFiles_WhenMultipleInvalidFiles_ShouldReturnEmptyList() { // Arrange var invalidFiles = new[] { Constants.NonExistentDll, Constants.NonExistentDll, Constants.NonExistentDll }; // Act - var messages = EventMessageProvider.GetMessages(invalidFiles, Constants.TestProviderName); + var messages = EventMessageProvider.LoadMessagesFromFiles(invalidFiles, Constants.TestProviderName); // Assert Assert.NotNull(messages); @@ -154,10 +154,10 @@ public void GetMessages_WhenMultipleInvalidFiles_ShouldReturnEmptyList() } [Fact] - public void GetMessages_WhenProviderNameProvided_ShouldIncludeInMessages() + public void LoadMessagesFromFiles_WhenProviderNameProvided_ShouldIncludeInMessages() { // Arrange & Act - var messages = EventMessageProvider.GetMessages([], Constants.TestProviderName); + var messages = EventMessageProvider.LoadMessagesFromFiles([], Constants.TestProviderName); // Assert Assert.NotNull(messages); diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Providers/ProviderDetailsTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Providers/ProviderDetailsTests.cs index 1d8bfff2..cb5b1279 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/Providers/ProviderDetailsTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/Providers/ProviderDetailsTests.cs @@ -1,7 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Providers; using EventLogExpert.Eventing.Tests.TestUtils; using EventLogExpert.Eventing.Tests.TestUtils.Constants; diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Readers/LogNamesTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Readers/LogNamesTests.cs deleted file mode 100644 index 47d0aa7a..00000000 --- a/tests/Unit/EventLogExpert.Eventing.Tests/Readers/LogNamesTests.cs +++ /dev/null @@ -1,36 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -using EventLogExpert.Eventing.Readers; - -namespace EventLogExpert.Eventing.Tests.Readers; - -public sealed class LogNamesTests -{ - [Fact] - public void AdminOnlyLiveLogNames_ContainsExpectedNames() - { - Assert.Contains(LogNames.SecurityLog, LogNames.AdminOnlyLiveLogNames); - Assert.Contains(LogNames.StateLog, LogNames.AdminOnlyLiveLogNames); - Assert.Equal(2, LogNames.AdminOnlyLiveLogNames.Count); - } - - [Theory] - [InlineData("security")] - [InlineData("SECURITY")] - [InlineData("state")] - [InlineData("STATE")] - public void AdminOnlyLiveLogNames_ShouldMatchCaseInsensitively(string input) - { - Assert.Contains(input, LogNames.AdminOnlyLiveLogNames); - } - - [Fact] - public void Constants_HaveExpectedValues() - { - Assert.Equal("Application", LogNames.ApplicationLog); - Assert.Equal("Security", LogNames.SecurityLog); - Assert.Equal("State", LogNames.StateLog); - Assert.Equal("System", LogNames.SystemLog); - } -} diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/EventResolvers/EventProviderDatabaseEventResolverTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventProviderDatabaseEventResolverTests.cs similarity index 67% rename from tests/Unit/EventLogExpert.Eventing.Tests/EventResolvers/EventProviderDatabaseEventResolverTests.cs rename to tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventProviderDatabaseEventResolverTests.cs index 4bd05857..68952262 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/EventResolvers/EventProviderDatabaseEventResolverTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventProviderDatabaseEventResolverTests.cs @@ -1,11 +1,11 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventProviderDatabase; -using EventLogExpert.Eventing.EventResolvers; +using EventLogExpert.Eventing.Common.Databases; using EventLogExpert.Eventing.Logging; -using EventLogExpert.Eventing.Models; -using EventLogExpert.Eventing.Providers; +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; @@ -13,7 +13,7 @@ using System.Collections.Concurrent; using System.Collections.Immutable; -namespace EventLogExpert.Eventing.Tests.EventResolvers; +namespace EventLogExpert.Eventing.Tests.Resolvers; public sealed class EventResolverDatabaseTests { @@ -21,7 +21,7 @@ public sealed class EventResolverDatabaseTests public void Constructor_WithCacheAndLogger_ShouldCreateInstance() { // Arrange - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns([]); var cache = Substitute.For(); var logger = Substitute.For(); @@ -37,7 +37,7 @@ public void Constructor_WithCacheAndLogger_ShouldCreateInstance() public void Constructor_WithLogger_ShouldLogDatabasePaths() { // Arrange - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns([Constants.NonExistentDatabaseFullPath]); var logger = Substitute.For(); @@ -61,7 +61,7 @@ public void Constructor_WithLogger_ShouldLogDatabasePaths() public void Constructor_WithLogger_ShouldLogInstantiation() { // Arrange - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns([]); var logger = Substitute.For(); @@ -77,7 +77,7 @@ public void Constructor_WithNonExistentDatabase_ShouldThrowFileNotFoundException { // Arrange string nonExistentPath = Path.Combine(Path.GetTempPath(), $"NonExistent_{Guid.NewGuid()}.db"); - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns(ImmutableList.Create(nonExistentPath)); // Act @@ -92,7 +92,7 @@ public void Constructor_WithNonExistentDatabase_ShouldThrowFileNotFoundException public void Constructor_WithNullCache_ShouldCreateInstance() { // Arrange - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns([]); // Act @@ -116,7 +116,7 @@ public void Constructor_WithNullDatabaseCollection_ShouldCreateInstance() public void Constructor_WithNullLogger_ShouldCreateInstance() { // Arrange - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns([]); // Act @@ -134,13 +134,13 @@ public void Constructor_WithValidDatabase_ShouldLoadDatabase() try { - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.ProviderDetails.Add(EventUtils.CreateProvider(Constants.TestProviderName)); context.SaveChanges(); } - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath)); // Act @@ -159,7 +159,7 @@ public void Constructor_WithValidDatabase_ShouldLoadDatabase() public void Dispose_CalledMultipleTimes_ShouldNotThrow() { // Arrange - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns([]); var resolver = new EventResolver(dbCollection); @@ -170,14 +170,14 @@ public void Dispose_CalledMultipleTimes_ShouldNotThrow() } [Fact] - public async Task Dispose_ConcurrentWithResolveProviderDetails_ShouldHandleThreadSafely() + public async Task Dispose_ConcurrentWithLoadProviderDetails_ShouldHandleThreadSafely() { // Arrange string dbPath = Path.Combine(Path.GetTempPath(), $"Test_{Guid.NewGuid()}.db"); try { - await using (var context = new EventProviderDbContext(dbPath, false)) + await using (var context = new ProviderDbContext(dbPath, false)) { for (int i = 0; i < 100; i++) { @@ -187,7 +187,7 @@ public async Task Dispose_ConcurrentWithResolveProviderDetails_ShouldHandleThrea await context.SaveChangesAsync(TestContext.Current.CancellationToken); } - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath)); var resolver = new EventResolver(dbCollection); @@ -207,7 +207,7 @@ public async Task Dispose_ConcurrentWithResolveProviderDetails_ShouldHandleThrea Id = 1000 }; - resolver.ResolveProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); } } catch (ObjectDisposedException) @@ -238,7 +238,7 @@ public async Task Dispose_ConcurrentWithResolveProviderDetails_ShouldHandleThrea public void Dispose_MultipleConcurrentDisposeCalls_ShouldHandleThreadSafely() { // Arrange - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns([]); var resolver = new EventResolver(dbCollection); @@ -262,47 +262,47 @@ public void Dispose_MultipleConcurrentDisposeCalls_ShouldHandleThreadSafely() } [Fact] - public void Dispose_ThenResolveEvent_ShouldThrowObjectDisposedException() + public void Dispose_ThenResolveEventViaBaseReference_ShouldThrowObjectDisposedException() { // Arrange - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns([]); - var resolver = new EventResolver(dbCollection); + EventResolver resolver = new EventResolver(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] - public void Dispose_ThenResolveEventViaBaseReference_ShouldThrowObjectDisposedException() + public void Dispose_ThenResolveEvent_ShouldThrowObjectDisposedException() { // Arrange - var dbCollection = Substitute.For(); + 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_ThenResolveProviderDetails_ShouldThrowObjectDisposedException() + public void Dispose_ThenLoadProviderDetails_ShouldThrowObjectDisposedException() { // Arrange - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns([]); var resolver = new EventResolver(dbCollection); @@ -316,25 +316,25 @@ public void Dispose_ThenResolveProviderDetails_ShouldThrowObjectDisposedExceptio resolver.Dispose(); // Assert - Assert.Throws(() => resolver.ResolveProviderDetails(eventRecord)); + Assert.Throws(() => resolver.LoadProviderDetails(eventRecord)); } [Fact] - public void ResolveProviderDetails_CalledTwiceForSameProvider_ShouldResolveOnlyOnce() + public void LoadProviderDetails_CalledTwiceForSameProvider_ShouldResolveOnlyOnce() { // Arrange string dbPath = Path.Combine(Path.GetTempPath(), $"Test_{Guid.NewGuid()}.db"); try { - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.ProviderDetails.Add(EventUtils.CreateProvider(Constants.TestProviderName)); context.SaveChanges(); } - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath)); var logger = Substitute.For(); @@ -347,8 +347,8 @@ public void ResolveProviderDetails_CalledTwiceForSameProvider_ShouldResolveOnlyO }; // Act - resolver.ResolveProviderDetails(eventRecord); - resolver.ResolveProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); // Assert logger.Received(1) @@ -361,14 +361,14 @@ public void ResolveProviderDetails_CalledTwiceForSameProvider_ShouldResolveOnlyO } [Fact] - public void ResolveProviderDetails_ConcurrentCalls_ShouldHandleThreadSafely() + public void LoadProviderDetails_ConcurrentCalls_ShouldHandleThreadSafely() { // Arrange string dbPath = Path.Combine(Path.GetTempPath(), $"Test_{Guid.NewGuid()}.db"); try { - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { for (int i = 0; i < 10; i++) { @@ -378,7 +378,7 @@ public void ResolveProviderDetails_ConcurrentCalls_ShouldHandleThreadSafely() context.SaveChanges(); } - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath)); using var resolver = new EventResolver(dbCollection); @@ -393,7 +393,7 @@ public void ResolveProviderDetails_ConcurrentCalls_ShouldHandleThreadSafely() Id = (ushort)(1000 + i) }; - resolver.ResolveProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); })); // Assert @@ -406,7 +406,7 @@ public void ResolveProviderDetails_ConcurrentCalls_ShouldHandleThreadSafely() } [Fact] - public void ResolveProviderDetails_MultipleConcurrentProvidersFromDifferentDatabases_ShouldHandleCorrectly() + public void LoadProviderDetails_MultipleConcurrentProvidersFromDifferentDatabases_ShouldHandleCorrectly() { // Arrange string dbPath1 = Path.Combine(Path.GetTempPath(), $"DB1_{Guid.NewGuid()}.db"); @@ -414,7 +414,7 @@ public void ResolveProviderDetails_MultipleConcurrentProvidersFromDifferentDatab try { - using (var context1 = new EventProviderDbContext(dbPath1, false)) + using (var context1 = new ProviderDbContext(dbPath1, false)) { for (int i = 0; i < 5; i++) { @@ -424,7 +424,7 @@ public void ResolveProviderDetails_MultipleConcurrentProvidersFromDifferentDatab context1.SaveChanges(); } - using (var context2 = new EventProviderDbContext(dbPath2, false)) + using (var context2 = new ProviderDbContext(dbPath2, false)) { for (int i = 0; i < 5; i++) { @@ -434,7 +434,7 @@ public void ResolveProviderDetails_MultipleConcurrentProvidersFromDifferentDatab context2.SaveChanges(); } - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath1, dbPath2)); using var resolver = new EventResolver(dbCollection); @@ -452,7 +452,7 @@ public void ResolveProviderDetails_MultipleConcurrentProvidersFromDifferentDatab Id = (ushort)(1000 + i) }; - resolver.ResolveProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); })); // Assert @@ -466,21 +466,21 @@ public void ResolveProviderDetails_MultipleConcurrentProvidersFromDifferentDatab } [Fact] - public void ResolveProviderDetails_WithCaseInsensitiveProviderName_ShouldResolve() + public void LoadProviderDetails_WithCaseInsensitiveProviderName_ShouldResolve() { // Arrange string dbPath = Path.Combine(Path.GetTempPath(), $"Test_{Guid.NewGuid()}.db"); try { - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.ProviderDetails.Add(EventUtils.CreateProvider(Constants.TestProviderName)); context.SaveChanges(); } - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath)); var logger = Substitute.For(); @@ -493,7 +493,7 @@ public void ResolveProviderDetails_WithCaseInsensitiveProviderName_ShouldResolve }; // Act - resolver.ResolveProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); // Assert logger.Received(1) @@ -506,7 +506,7 @@ public void ResolveProviderDetails_WithCaseInsensitiveProviderName_ShouldResolve } [Fact] - public void ResolveProviderDetails_WithFirstDatabaseContainingProvider_ShouldUseFirstDatabase() + public void LoadProviderDetails_WithFirstDatabaseContainingProvider_ShouldUseFirstDatabase() { // Arrange string dbPath1 = Path.Combine(Path.GetTempPath(), $"Exchange 2019_{Guid.NewGuid()}.db"); @@ -514,21 +514,21 @@ public void ResolveProviderDetails_WithFirstDatabaseContainingProvider_ShouldUse try { - using (var context1 = new EventProviderDbContext(dbPath1, false)) + using (var context1 = new ProviderDbContext(dbPath1, false)) { context1.ProviderDetails.Add(EventUtils.CreateProvider(Constants.TestProviderName)); context1.SaveChanges(); } - using (var context2 = new EventProviderDbContext(dbPath2, false)) + using (var context2 = new ProviderDbContext(dbPath2, false)) { context2.ProviderDetails.Add(EventUtils.CreateProvider(Constants.TestProviderName)); context2.SaveChanges(); } - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath1, dbPath2)); var logger = Substitute.For(); @@ -541,7 +541,7 @@ public void ResolveProviderDetails_WithFirstDatabaseContainingProvider_ShouldUse }; // Act - resolver.ResolveProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); // Assert // Should use Exchange database (first in sorted order) @@ -558,21 +558,21 @@ public void ResolveProviderDetails_WithFirstDatabaseContainingProvider_ShouldUse } [Fact] - public void ResolveProviderDetails_WithKnownProvider_ShouldResolveFromDatabase() + public void LoadProviderDetails_WithKnownProvider_ShouldResolveFromDatabase() { // Arrange string dbPath = Path.Combine(Path.GetTempPath(), $"Test_{Guid.NewGuid()}.db"); try { - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.ProviderDetails.Add(EventUtils.CreateProvider(Constants.TestProviderName)); context.SaveChanges(); } - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath)); var logger = Substitute.For(); @@ -585,7 +585,7 @@ public void ResolveProviderDetails_WithKnownProvider_ShouldResolveFromDatabase() }; // Act - resolver.ResolveProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); // Assert logger.Received(1).Debug(Arg.Is(h => @@ -600,7 +600,7 @@ public void ResolveProviderDetails_WithKnownProvider_ShouldResolveFromDatabase() } [Fact] - public void ResolveProviderDetails_WithMultipleDatabases_ShouldCheckAllDatabases() + public void LoadProviderDetails_WithMultipleDatabases_ShouldCheckAllDatabases() { // Arrange string dbPath1 = Path.Combine(Path.GetTempPath(), $"Test1_{Guid.NewGuid()}.db"); @@ -608,21 +608,21 @@ public void ResolveProviderDetails_WithMultipleDatabases_ShouldCheckAllDatabases try { - using (var context1 = new EventProviderDbContext(dbPath1, false)) + using (var context1 = new ProviderDbContext(dbPath1, false)) { context1.ProviderDetails.Add(EventUtils.CreateProvider("Provider1")); context1.SaveChanges(); } - using (var context2 = new EventProviderDbContext(dbPath2, false)) + using (var context2 = new ProviderDbContext(dbPath2, false)) { context2.ProviderDetails.Add(EventUtils.CreateProvider("Provider2")); context2.SaveChanges(); } - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath1, dbPath2)); using var resolver = new EventResolver(dbCollection); @@ -633,8 +633,8 @@ public void ResolveProviderDetails_WithMultipleDatabases_ShouldCheckAllDatabases // Act var exception = Record.Exception(() => { - resolver.ResolveProviderDetails(eventRecord1); - resolver.ResolveProviderDetails(eventRecord2); + resolver.LoadProviderDetails(eventRecord1); + resolver.LoadProviderDetails(eventRecord2); }); // Assert @@ -648,7 +648,7 @@ public void ResolveProviderDetails_WithMultipleDatabases_ShouldCheckAllDatabases } [Fact] - public void ResolveProviderDetails_WithProviderInLaterDatabase_ShouldFindProvider() + public void LoadProviderDetails_WithProviderInLaterDatabase_ShouldFindProvider() { // Arrange string dbPath1 = Path.Combine(Path.GetTempPath(), $"Empty_{Guid.NewGuid()}.db"); @@ -656,20 +656,20 @@ public void ResolveProviderDetails_WithProviderInLaterDatabase_ShouldFindProvide try { - using (var context1 = new EventProviderDbContext(dbPath1, false)) + using (var context1 = new ProviderDbContext(dbPath1, false)) { // Empty database context1.SaveChanges(); } - using (var context2 = new EventProviderDbContext(dbPath2, false)) + using (var context2 = new ProviderDbContext(dbPath2, false)) { context2.ProviderDetails.Add(EventUtils.CreateProvider(Constants.TestProviderName)); context2.SaveChanges(); } - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath1, dbPath2)); var logger = Substitute.For(); @@ -682,7 +682,7 @@ public void ResolveProviderDetails_WithProviderInLaterDatabase_ShouldFindProvide }; // Act - resolver.ResolveProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); // Assert logger.Received(1).Debug(Arg.Is(h => @@ -697,10 +697,10 @@ public void ResolveProviderDetails_WithProviderInLaterDatabase_ShouldFindProvide } [Fact] - public void ResolveProviderDetails_WithUnknownProvider_ShouldAddEmptyDetails() + public void LoadProviderDetails_WithUnknownProvider_ShouldAddEmptyDetails() { // Arrange - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns([]); using var resolver = new EventResolver(dbCollection); @@ -714,170 +714,16 @@ public void ResolveProviderDetails_WithUnknownProvider_ShouldAddEmptyDetails() // Act var exception = Record.Exception(() => { - resolver.ResolveProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); // Second call should not throw (provider details should be cached) - resolver.ResolveProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); }); // Assert Assert.Null(exception); } - [Fact] - public void SortDatabases_PreservesDirectory_ShouldReturnFullPaths() - { - // Arrange - var databases = new[] - { - @"C:\Databases\Test\Windows 2019.db", - @"C:\Databases\Prod\Exchange 2016.db" - }; - - // Act - var sorted = EventResolver.SortDatabases(databases).ToList(); - - // Assert - Assert.All(sorted, path => Assert.True(Path.IsPathRooted(path))); - Assert.Contains(sorted, s => s.Contains(@"Databases\Test")); - Assert.Contains(sorted, s => s.Contains(@"Databases\Prod")); - } - - [Fact] - public void SortDatabases_WithComplexVersionStrings_ShouldHandleCorrectly() - { - // Arrange - var databases = new[] - { - @"C:\Test\Product v2.0.db", - @"C:\Test\Product v1.5.db", - @"C:\Test\Product RC1.db" - }; - - // Act - var sorted = EventResolver.SortDatabases(databases).ToList(); - - // Assert - Assert.Equal(3, sorted.Count); - // Should be sorted by second part descending - Assert.Equal(@"C:\Test\Product v2.0.db", sorted[0]); - Assert.Equal(@"C:\Test\Product v1.5.db", sorted[1]); - Assert.Equal(@"C:\Test\Product RC1.db", sorted[2]); - } - - [Fact] - public void SortDatabases_WithEmptyList_ShouldReturnEmptyList() - { - // Act - var sorted = EventResolver.SortDatabases([]); - - // Assert - Assert.Empty(sorted); - } - - [Fact] - public void SortDatabases_WithMixedVersionsAndNoVersions_ShouldSortCorrectly() - { - // Arrange - var databases = new[] - { - @"C:\Test\Windows 2019.db", - @"C:\Test\Exchange.db", - @"C:\Test\Windows 2016.db", - @"C:\Test\Azure 2020.db" - }; - - // Act - var sorted = EventResolver.SortDatabases(databases).ToList(); - - // Assert - Assert.Equal(4, sorted.Count); - Assert.Equal(@"C:\Test\Azure 2020.db", sorted[0]); - Assert.Equal(@"C:\Test\Exchange.db", sorted[1]); - Assert.Equal(@"C:\Test\Windows 2019.db", sorted[2]); - Assert.Equal(@"C:\Test\Windows 2016.db", sorted[3]); - } - - [Fact] - public void SortDatabases_WithNoVersion_ShouldSortByProductName() - { - // Arrange - var databases = new[] - { - @"C:\Test\Windows.db", - @"C:\Test\Exchange.db", - @"C:\Test\Azure.db" - }; - - // Act - var sorted = EventResolver.SortDatabases(databases).ToList(); - - // Assert - Assert.Equal(3, sorted.Count); - Assert.Equal(@"C:\Test\Azure.db", sorted[0]); - Assert.Equal(@"C:\Test\Exchange.db", sorted[1]); - Assert.Equal(@"C:\Test\Windows.db", sorted[2]); - } - - [Fact] - public void SortDatabases_WithNumericVersions_ShouldSortDescending() - { - // Arrange - var databases = new[] - { - @"C:\Test\Product 10.db", - @"C:\Test\Product 2.db", - @"C:\Test\Product 20.db" - }; - - // Act - var sorted = EventResolver.SortDatabases(databases).ToList(); - - // Assert - // Numeric comparison (descending): 20 > 10 > 2 - Assert.Equal(3, sorted.Count); - Assert.Equal(@"C:\Test\Product 20.db", sorted[0]); - Assert.Equal(@"C:\Test\Product 10.db", sorted[1]); - Assert.Equal(@"C:\Test\Product 2.db", sorted[2]); - } - - [Fact] - public void SortDatabases_WithProductNameAndVersion_ShouldSortByProductAscendingVersionDescending() - { - // Arrange - var databases = new[] - { - @"C:\Test\Windows 2016.db", - @"C:\Test\Windows 2019.db", - @"C:\Test\Exchange 2019.db", - @"C:\Test\Exchange 2016.db" - }; - - // Act - var sorted = EventResolver.SortDatabases(databases).ToList(); - - // Assert - Assert.Equal(4, sorted.Count); - Assert.Equal(@"C:\Test\Exchange 2019.db", sorted[0]); - Assert.Equal(@"C:\Test\Exchange 2016.db", sorted[1]); - Assert.Equal(@"C:\Test\Windows 2019.db", sorted[2]); - Assert.Equal(@"C:\Test\Windows 2016.db", sorted[3]); - } - - [Fact] - public void SortDatabases_WithSingleDatabase_ShouldReturnSameDatabase() - { - // Arrange - var databases = new[] { @"C:\Test\Windows 2019.db" }; - - // Act - var sorted = EventResolver.SortDatabases(databases).ToList(); - - // Assert - Assert.Single(sorted); - Assert.Equal(@"C:\Test\Windows 2019.db", sorted[0]); - } - private static void DeleteDatabaseFile(string path, int maxRetries = 10) { if (!File.Exists(path)) { return; } diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventResolverBaseTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs rename to tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventResolverBaseTests.cs index 8bfe83e3..5e593671 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventResolverBaseTests.cs @@ -1,17 +1,17 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventResolvers; using EventLogExpert.Eventing.Interop; using EventLogExpert.Eventing.Logging; -using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Providers; +using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Resolvers; using EventLogExpert.Eventing.Tests.TestUtils; using EventLogExpert.Eventing.Tests.TestUtils.Constants; using NSubstitute; using System.Collections.Concurrent; -namespace EventLogExpert.Eventing.Tests.EventResolvers; +namespace EventLogExpert.Eventing.Tests.Resolvers; public sealed class EventResolverBaseTests { @@ -84,7 +84,7 @@ public void ResolveEvent_ConcurrentCalls_ShouldHandleThreadSafely() LogName = Constants.ApplicationLogName }; - resolver.ResolveProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); var displayEvent = resolver.ResolveEvent(eventRecord); Assert.NotNull(displayEvent); } @@ -251,19 +251,19 @@ public void ResolveEvent_WithAmbiguousPrimaryAndSupplementalModernEventTask_Shou } [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(), @@ -278,20 +278,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); @@ -299,12 +298,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 @@ -312,25 +310,23 @@ 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] - 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(), @@ -345,19 +341,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); @@ -365,11 +362,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 @@ -377,7 +375,9 @@ 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] @@ -595,6 +595,44 @@ 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() { @@ -654,44 +692,6 @@ 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,6 +961,26 @@ 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() { @@ -1066,26 +1086,6 @@ 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() { @@ -1361,7 +1361,7 @@ public void ResolveEvent_WithLogNameMismatch_ShouldFallbackToUniqueIdVersionMatc [Fact] public void ResolveEvent_WithLogNameMismatch_ShouldNotMatchWhenAmbiguous() { - // Arrange - two events with same Id+Version but different LogNames, neither matching + // Arrange - two events with same Id+Version but different LogChannelNames, neither matching var providerDetails = new ProviderDetails { ProviderName = Constants.TestProviderName, @@ -1763,6 +1763,59 @@ 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() { @@ -1898,59 +1951,6 @@ 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,6 +2052,29 @@ 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() { @@ -2095,29 +2118,6 @@ 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.ResolveProviderDetails(eventRecord); - var displayEvent = resolver.ResolveEvent(eventRecord); - - // Assert - Assert.NotNull(displayEvent); - Assert.Contains("No matching provider", displayEvent.Description); - } - [Fact] public void ResolveEvent_WithNormalMismatch_ShouldStillReject() { @@ -2222,7 +2222,7 @@ public void ResolveEvent_WithNullKeywords_ShouldReturnEmptyKeywordList() }; // Act - resolver.ResolveProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); var displayEvent = resolver.ResolveEvent(eventRecord); // Assert @@ -2808,9 +2808,9 @@ public void ResolveEvent_WithProviderHavingOnlyKeywordsTasksOpcodes_ShouldReturn } [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, @@ -2819,8 +2819,8 @@ public void ResolveEvent_WithProviderKeywords_ShouldResolveKeywords() Parameters = [], Keywords = new Dictionary { - { 0x1, "CustomKeyword1" }, - { 0x2, "CustomKeyword2" } + { 0x100000000L, "HighBitKeyword" }, + { 0x1, "LowBitKeyword" } }, Tasks = new Dictionary() }; @@ -2831,7 +2831,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 @@ -2839,14 +2839,14 @@ 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] - 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, @@ -2855,8 +2855,8 @@ public void ResolveEvent_WithProviderKeywordsInBits32To47_ShouldResolveKeywords( Parameters = [], Keywords = new Dictionary { - { 0x100000000L, "HighBitKeyword" }, - { 0x1, "LowBitKeyword" } + { 0x1, "CustomKeyword1" }, + { 0x2, "CustomKeyword2" } }, Tasks = new Dictionary() }; @@ -2867,7 +2867,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 @@ -2875,8 +2875,8 @@ 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] @@ -3229,7 +3229,7 @@ public void ResolveEvent_WithStandardKeywords_ShouldResolveKeywords() }; // Act - resolver.ResolveProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); var displayEvent = resolver.ResolveEvent(eventRecord); // Assert @@ -3537,7 +3537,7 @@ public void ResolveEvent_WithZeroKeywords_ShouldReturnEmptyKeywordList() }; // Act - resolver.ResolveProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); var displayEvent = resolver.ResolveEvent(eventRecord); // Assert @@ -3560,7 +3560,7 @@ public SupplementalTestResolver( providerDetailsList.ForEach(p => ProviderDetails.TryAdd(p.ProviderName, p)); } - public void ResolveProviderDetails(EventRecord eventRecord) + public void LoadProviderDetails(EventRecord eventRecord) { if (ProviderDetails.ContainsKey(eventRecord.ProviderName)) { @@ -3591,7 +3591,7 @@ public TestEventResolver( public ConcurrentDictionary GetProviderDetails() => ProviderDetails; - public void ResolveProviderDetails(EventRecord eventRecord) + public void LoadProviderDetails(EventRecord eventRecord) { if (ProviderDetails.ContainsKey(eventRecord.ProviderName)) { diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverCacheTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventResolverCacheTests.cs similarity index 98% rename from tests/Unit/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverCacheTests.cs rename to tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventResolverCacheTests.cs index 958f8490..43ce5dd6 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverCacheTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventResolverCacheTests.cs @@ -1,9 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventResolvers; +using EventLogExpert.Eventing.Resolvers; -namespace EventLogExpert.Eventing.Tests.EventResolvers; +namespace EventLogExpert.Eventing.Tests.Resolvers; public sealed class EventResolverCacheTests { diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/EventResolvers/EventXmlResolverTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventXmlResolverTests.cs similarity index 93% rename from tests/Unit/EventLogExpert.Eventing.Tests/EventResolvers/EventXmlResolverTests.cs rename to tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventXmlResolverTests.cs index 616d9b04..5e2799eb 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/EventResolvers/EventXmlResolverTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventXmlResolverTests.cs @@ -1,11 +1,11 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventResolvers; -using EventLogExpert.Eventing.Models; -using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Common.Channels; +using EventLogExpert.Eventing.Common.Events; +using EventLogExpert.Eventing.Resolvers; -namespace EventLogExpert.Eventing.Tests.EventResolvers; +namespace EventLogExpert.Eventing.Tests.Resolvers; public sealed class EventXmlResolverTests { @@ -25,7 +25,7 @@ public async Task ClearAll_RemovesEveryEntry() } [Fact] - public async Task ClearLog_RemovesEntriesForThatLogOnly() + public async Task ClearXmlCacheForLog_RemovesEntriesForThatLogOnly() { var resolver = CreateTrackingResolver( key => $"", @@ -40,7 +40,7 @@ public async Task ClearLog_RemovesEntriesForThatLogOnly() await resolver.GetXmlAsync(evtB1, TestContext.Current.CancellationToken); Assert.Equal(3, getResolveCallCount()); - resolver.ClearLog("A"); + resolver.ClearXmlCacheForLog("A"); // B is untouched. await resolver.GetXmlAsync(evtB1, TestContext.Current.CancellationToken); @@ -194,18 +194,18 @@ private static EventXmlResolver CreateBoundedTrackingResolver( getResolveCallCount = () => Volatile.Read(ref resolveCount); return new EventXmlResolver( - (owningLog, recordId, pathType) => + (owningLog, recordId, LogPathType) => { Interlocked.Increment(ref resolveCount); - return resolve(new ResolveKey(owningLog, recordId, pathType)); + return resolve(new ResolveKey(owningLog, recordId, LogPathType)); }, initialCapacity, maxCapacity); } private static DisplayEventModel CreateEvent(long? recordId, string owningLog = "TestLog") => - new(owningLog, PathType.LogName) + new(owningLog, LogPathType.Channel) { RecordId = recordId, Xml = string.Empty @@ -220,13 +220,13 @@ private static EventXmlResolver CreateTrackingResolver( getResolveCallCount = () => Volatile.Read(ref resolveCount); return new EventXmlResolver( - (owningLog, recordId, pathType) => + (owningLog, recordId, LogPathType) => { Interlocked.Increment(ref resolveCount); - return resolve(new ResolveKey(owningLog, recordId, pathType)); + return resolve(new ResolveKey(owningLog, recordId, LogPathType)); }); } - private readonly record struct ResolveKey(string OwningLog, long RecordId, PathType PathType); + private readonly record struct ResolveKey(string OwningLog, long RecordId, LogPathType LogPathType); } diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/EventResolvers/LocalProviderEventResolverTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/LocalProviderEventResolverTests.cs similarity index 83% rename from tests/Unit/EventLogExpert.Eventing.Tests/EventResolvers/LocalProviderEventResolverTests.cs rename to tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/LocalProviderEventResolverTests.cs index 2883e4cc..aae01abd 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/EventResolvers/LocalProviderEventResolverTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/LocalProviderEventResolverTests.cs @@ -1,15 +1,15 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventResolvers; using EventLogExpert.Eventing.Logging; -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Resolvers; using EventLogExpert.Eventing.Tests.TestUtils; using EventLogExpert.Eventing.Tests.TestUtils.Constants; using NSubstitute; using System.Collections.Concurrent; -namespace EventLogExpert.Eventing.Tests.EventResolvers; +namespace EventLogExpert.Eventing.Tests.Resolvers; public sealed class EventResolverLocalProviderTests { @@ -124,7 +124,7 @@ public void Dispose_ThenResolveEvent_ShouldThrowObjectDisposedException() } [Fact] - public void Dispose_ThenResolveProviderDetails_ShouldThrowObjectDisposedException() + public void Dispose_ThenLoadProviderDetails_ShouldThrowObjectDisposedException() { // Arrange var resolver = new EventResolver(); @@ -134,7 +134,7 @@ public void Dispose_ThenResolveProviderDetails_ShouldThrowObjectDisposedExceptio resolver.Dispose(); // Assert - Assert.Throws(() => resolver.ResolveProviderDetails(eventRecord)); + Assert.Throws(() => resolver.LoadProviderDetails(eventRecord)); } [Fact] @@ -147,8 +147,8 @@ public void MultipleResolvers_WithDifferentInstances_ShouldResolveSeparately() var eventRecord = EventUtils.CreateBasicEvent(); // Act - resolver1.ResolveProviderDetails(eventRecord); - resolver2.ResolveProviderDetails(eventRecord); + resolver1.LoadProviderDetails(eventRecord); + resolver2.LoadProviderDetails(eventRecord); var displayEvent1 = resolver1.ResolveEvent(eventRecord); var displayEvent2 = resolver2.ResolveEvent(eventRecord); @@ -169,10 +169,10 @@ public void MultipleResolvers_WithSharedCache_ShouldShareCachedStrings() var eventRecord = EventUtils.CreateBasicEvent(); // Act - resolver1.ResolveProviderDetails(eventRecord); + resolver1.LoadProviderDetails(eventRecord); var displayEvent1 = resolver1.ResolveEvent(eventRecord); - resolver2.ResolveProviderDetails(eventRecord); + resolver2.LoadProviderDetails(eventRecord); var displayEvent2 = resolver2.ResolveEvent(eventRecord); // Assert @@ -192,8 +192,8 @@ public void ResolveEvent_WithMultipleProviders_ShouldResolveEachCorrectly() var eventRecords = EventUtils.CreateDifferentEvents().ToList(); // Act - resolver.ResolveProviderDetails(eventRecords[0]); - resolver.ResolveProviderDetails(eventRecords[1]); + resolver.LoadProviderDetails(eventRecords[0]); + resolver.LoadProviderDetails(eventRecords[1]); var displayEvent1 = resolver.ResolveEvent(eventRecords[0]); var displayEvent2 = resolver.ResolveEvent(eventRecords[1]); @@ -206,7 +206,7 @@ public void ResolveEvent_WithMultipleProviders_ShouldResolveEachCorrectly() } [Fact] - public void ResolveEvent_WithoutCallingResolveProviderDetails_ShouldStillResolve() + public void ResolveEvent_WithoutCallingLoadProviderDetails_ShouldStillResolve() { // Arrange var resolver = new EventResolver(); @@ -223,50 +223,37 @@ public void ResolveEvent_WithoutCallingResolveProviderDetails_ShouldStillResolve } [Fact] - public void ResolveProviderDetails_CalledTwiceForSameProvider_ShouldNotThrow() + public void LoadProviderDetails_CalledTwiceForSameProvider_ShouldNotThrow() { // Arrange var resolver = new EventResolver(); var eventRecord = EventUtils.CreateBasicEvent(); // Act - resolver.ResolveProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); // Second call should use cached provider details - var exception = Record.Exception(() => resolver.ResolveProviderDetails(eventRecord)); + var exception = Record.Exception(() => resolver.LoadProviderDetails(eventRecord)); // Assert Assert.Null(exception); } [Fact] - public void ResolveProviderDetails_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) - }; - - resolver.ResolveProviderDetails(eventRecord); + var eventRecord = EventUtils.CreateBasicEvent(); + eventRecord.Id = (ushort)(1000 + i); + resolver.LoadProviderDetails(eventRecord); } catch (Exception ex) { @@ -279,20 +266,33 @@ public void ResolveProviderDetails_ConcurrentCalls_ShouldHandleThreadSafely() } [Fact] - public void ResolveProviderDetails_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); - resolver.ResolveProviderDetails(eventRecord); + var eventRecord = new EventRecord + { + ProviderName = providerNames[i % providerNames.Length], + Id = (ushort)(1000 + i) + }; + + resolver.LoadProviderDetails(eventRecord); } catch (Exception ex) { @@ -305,7 +305,7 @@ public void ResolveProviderDetails_ConcurrentCallsForSameProvider_ShouldHandleTh } [Fact] - public void ResolveProviderDetails_MixedConcurrentProvidersWithCaching_ShouldHandleThreadSafely() + public void LoadProviderDetails_MixedConcurrentProvidersWithCaching_ShouldHandleThreadSafely() { // Arrange var cache = new EventResolverCache(); @@ -331,7 +331,7 @@ public void ResolveProviderDetails_MixedConcurrentProvidersWithCaching_ShouldHan Id = (ushort)(1000 + i) }; - resolver.ResolveProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); } catch (Exception ex) { @@ -344,14 +344,14 @@ public void ResolveProviderDetails_MixedConcurrentProvidersWithCaching_ShouldHan } [Fact] - public void ResolveProviderDetails_ThenResolveEvent_ShouldReturnPopulatedModel() + public void LoadProviderDetails_ThenResolveEvent_ShouldReturnPopulatedModel() { // Arrange var resolver = new EventResolver(); var eventRecord = EventUtils.CreateBasicEvent(); // Act - resolver.ResolveProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); var displayEvent = resolver.ResolveEvent(eventRecord); // Assert @@ -366,7 +366,7 @@ public void ResolveProviderDetails_ThenResolveEvent_ShouldReturnPopulatedModel() } [Fact] - public void ResolveProviderDetails_WithCache_ThenResolveEvent_ShouldUseCachedStrings() + public void LoadProviderDetails_WithCache_ThenResolveEvent_ShouldUseCachedStrings() { // Arrange var cache = new EventResolverCache(); @@ -374,7 +374,7 @@ public void ResolveProviderDetails_WithCache_ThenResolveEvent_ShouldUseCachedStr var eventRecord = EventUtils.CreateBasicEvent(); // Act - resolver.ResolveProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); var displayEvent1 = resolver.ResolveEvent(eventRecord); var displayEvent2 = resolver.ResolveEvent(eventRecord); @@ -388,15 +388,15 @@ public void ResolveProviderDetails_WithCache_ThenResolveEvent_ShouldUseCachedStr } [Fact] - public void ResolveProviderDetails_WithDifferentProviders_ShouldResolveEachIndependently() + public void LoadProviderDetails_WithDifferentProviders_ShouldResolveEachIndependently() { // Arrange var resolver = new EventResolver(); var eventRecords = EventUtils.CreateDifferentEvents().ToList(); // Act - resolver.ResolveProviderDetails(eventRecords[0]); - resolver.ResolveProviderDetails(eventRecords[1]); + resolver.LoadProviderDetails(eventRecords[0]); + resolver.LoadProviderDetails(eventRecords[1]); var displayEvent1 = resolver.ResolveEvent(eventRecords[0]); var displayEvent2 = resolver.ResolveEvent(eventRecords[1]); @@ -408,7 +408,7 @@ public void ResolveProviderDetails_WithDifferentProviders_ShouldResolveEachIndep } [Fact] - public void ResolveProviderDetails_WithEmptyProviderName_ShouldResolveWithoutException() + public void LoadProviderDetails_WithEmptyProviderName_ShouldResolveWithoutException() { // Arrange var resolver = new EventResolver(); @@ -422,7 +422,7 @@ public void ResolveProviderDetails_WithEmptyProviderName_ShouldResolveWithoutExc }; // Act - resolver.ResolveProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); var displayEvent = resolver.ResolveEvent(eventRecord); // Assert @@ -431,7 +431,7 @@ public void ResolveProviderDetails_WithEmptyProviderName_ShouldResolveWithoutExc } [Fact] - public void ResolveProviderDetails_WithNonExistentProvider_ShouldResolveWithDefaultDescription() + public void LoadProviderDetails_WithNonExistentProvider_ShouldResolveWithDefaultDescription() { // Arrange var resolver = new EventResolver(); @@ -446,7 +446,7 @@ public void ResolveProviderDetails_WithNonExistentProvider_ShouldResolveWithDefa }; // Act - resolver.ResolveProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); var displayEvent = resolver.ResolveEvent(eventRecord); // Assert diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/EventResolvers/VersatileEventResolverTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/VersatileEventResolverTests.cs similarity index 87% rename from tests/Unit/EventLogExpert.Eventing.Tests/EventResolvers/VersatileEventResolverTests.cs rename to tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/VersatileEventResolverTests.cs index 78062f4e..e3c3373a 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/EventResolvers/VersatileEventResolverTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/VersatileEventResolverTests.cs @@ -1,18 +1,20 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventProviderDatabase; -using EventLogExpert.Eventing.EventResolvers; +using EventLogExpert.Eventing.Common.Databases; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.Eventing.Logging; -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.ProviderDatabase; 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 NSubstitute; using System.Collections.Immutable; -namespace EventLogExpert.Eventing.Tests.EventResolvers; +namespace EventLogExpert.Eventing.Tests.Resolvers; public sealed class EventResolverTests { @@ -38,12 +40,12 @@ public void Constructor_WithDatabaseCollection_ShouldUseDatabaseResolver() try { - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.SaveChanges(); } - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath)); // Act & Assert @@ -68,7 +70,7 @@ public void Constructor_WithDatabaseCollection_ShouldUseDatabaseResolver() public void Constructor_WithEmptyDatabaseCollection_ShouldUseLocalResolver() { // Arrange - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns(ImmutableList.Empty); // Act @@ -121,12 +123,12 @@ public void Dispose_ThenResolveEvent_WithDatabaseResolver_ShouldThrowObjectDispo try { - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.SaveChanges(); } - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath)); var resolver = new EventResolver(dbCollection); @@ -165,19 +167,19 @@ public void Dispose_ThenResolveEvent_WithLocalResolver_ShouldThrowObjectDisposed } [Fact] - public void Dispose_ThenResolveProviderDetails_WithDatabaseResolver_ShouldThrowObjectDisposedException() + public void Dispose_ThenLoadProviderDetails_WithDatabaseResolver_ShouldThrowObjectDisposedException() { // Arrange string dbPath = Path.Combine(Path.GetTempPath(), $"Test_{Guid.NewGuid()}.db"); try { - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.SaveChanges(); } - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath)); var resolver = new EventResolver(dbCollection); @@ -191,7 +193,7 @@ public void Dispose_ThenResolveProviderDetails_WithDatabaseResolver_ShouldThrowO resolver.Dispose(); // Assert - Assert.Throws(() => resolver.ResolveProviderDetails(eventRecord)); + Assert.Throws(() => resolver.LoadProviderDetails(eventRecord)); } finally { @@ -206,7 +208,7 @@ public void Dispose_ThenResolveProviderDetails_WithDatabaseResolver_ShouldThrowO } [Fact] - public void Dispose_ThenResolveProviderDetails_WithLocalResolver_ShouldThrowObjectDisposedException() + public void Dispose_ThenLoadProviderDetails_WithLocalResolver_ShouldThrowObjectDisposedException() { // Arrange var resolver = new EventResolver(); // Uses local resolver @@ -220,7 +222,7 @@ public void Dispose_ThenResolveProviderDetails_WithLocalResolver_ShouldThrowObje resolver.Dispose(); // Assert - Should throw even though local resolver doesn't hold resources - Assert.Throws(() => resolver.ResolveProviderDetails(eventRecord)); + Assert.Throws(() => resolver.LoadProviderDetails(eventRecord)); } [Fact] @@ -282,7 +284,7 @@ public void ResolveEvent_WithDatabaseResolver_ShouldResolveEvent() try { - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.ProviderDetails.Add(new ProviderDetails { @@ -293,7 +295,7 @@ public void ResolveEvent_WithDatabaseResolver_ShouldResolveEvent() context.SaveChanges(); } - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath)); var eventRecord = new EventRecord @@ -356,7 +358,7 @@ public void ResolveEvent_WithMixedDbAndUnknownProviders_ShouldOnlyApplyDatabaseT try { - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.ProviderDetails.Add(new ProviderDetails { @@ -375,7 +377,7 @@ public void ResolveEvent_WithMixedDbAndUnknownProviders_ShouldOnlyApplyDatabaseT context.SaveChanges(); } - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath)); DisplayEventModel dbDisplayEvent; @@ -478,7 +480,7 @@ public void ResolveEvent_WithProvider_ShouldReturnDisplayEventWithMatchingFields } [Fact] - public void ResolveProviderDetails_CalledTwice_ShouldHandleCorrectly() + public void LoadProviderDetails_CalledTwice_ShouldHandleCorrectly() { // Arrange using var resolver = new EventResolver(); @@ -490,15 +492,15 @@ public void ResolveProviderDetails_CalledTwice_ShouldHandleCorrectly() }; // Act - resolver.ResolveProviderDetails(eventRecord); - var exception = Record.Exception(() => resolver.ResolveProviderDetails(eventRecord)); + resolver.LoadProviderDetails(eventRecord); + var exception = Record.Exception(() => resolver.LoadProviderDetails(eventRecord)); // Assert Assert.Null(exception); } [Fact] - public void ResolveProviderDetails_ConcurrentCalls_ShouldHandleThreadSafely() + public void LoadProviderDetails_ConcurrentCalls_ShouldHandleThreadSafely() { // Arrange using var resolver = new EventResolver(); @@ -515,7 +517,7 @@ public void ResolveProviderDetails_ConcurrentCalls_ShouldHandleThreadSafely() Id = (ushort)(1000 + i) }; - resolver.ResolveProviderDetails(eventRecord); + resolver.LoadProviderDetails(eventRecord); } catch (Exception ex) { @@ -528,19 +530,19 @@ public void ResolveProviderDetails_ConcurrentCalls_ShouldHandleThreadSafely() } [Fact] - public void ResolveProviderDetails_WithDatabaseResolver_ShouldResolve() + public void LoadProviderDetails_WithDatabaseResolver_ShouldResolve() { // Arrange string dbPath = Path.Combine(Path.GetTempPath(), $"Test_{Guid.NewGuid()}.db"); try { - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.SaveChanges(); } - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath)); var eventRecord = new EventRecord @@ -553,7 +555,7 @@ public void ResolveProviderDetails_WithDatabaseResolver_ShouldResolve() Exception? exception; using (var resolver = new EventResolver(dbCollection)) { - exception = Record.Exception(() => resolver.ResolveProviderDetails(eventRecord)); + exception = Record.Exception(() => resolver.LoadProviderDetails(eventRecord)); } // Assert @@ -572,7 +574,7 @@ public void ResolveProviderDetails_WithDatabaseResolver_ShouldResolve() } [Fact] - public void ResolveProviderDetails_WithLocalResolver_ShouldResolve() + public void LoadProviderDetails_WithLocalResolver_ShouldResolve() { // Arrange using var resolver = new EventResolver(); @@ -584,7 +586,7 @@ public void ResolveProviderDetails_WithLocalResolver_ShouldResolve() }; // Act - var exception = Record.Exception(() => resolver.ResolveProviderDetails(eventRecord)); + var exception = Record.Exception(() => resolver.LoadProviderDetails(eventRecord)); // Assert Assert.Null(exception); @@ -598,12 +600,12 @@ public void SwitchBetweenResolvers_WithDifferentInstances_ShouldWorkIndependentl try { - using (var context = new EventProviderDbContext(dbPath, false)) + using (var context = new ProviderDbContext(dbPath, false)) { context.SaveChanges(); } - var dbCollection = Substitute.For(); + var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath)); var eventRecord = EventUtils.CreateBasicEvent(); diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/EventUtils.cs b/tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/EventUtils.cs index 4865d3c1..88cfd6b2 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/EventUtils.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/EventUtils.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Providers; +using EventLogExpert.Eventing.Readers; using static EventLogExpert.Eventing.Tests.TestUtils.Constants.Constants; namespace EventLogExpert.Eventing.Tests.TestUtils; diff --git a/tests/Unit/EventLogExpert.UI.Tests/DateRangeDefaultsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/DateRangeDefaultsTests.cs index f5a0f6c5..3f95cd46 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/DateRangeDefaultsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/DateRangeDefaultsTests.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; -using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Common.Channels; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.UI.Models; using EventLogExpert.UI.Tests.TestUtils; @@ -15,7 +15,7 @@ public sealed class DateRangeDefaultsTests [Fact] public void ComputeFromActiveLogs_WhenAllLogsEmpty_ReturnsRoundedFallback() { - var emptyLog = new EventLogData("Empty", PathType.LogName, []); + var emptyLog = new EventLogData("Empty", LogPathType.Channel, []); var (after, before) = DateRangeDefaults.ComputeFromActiveLogs([emptyLog], s_fallbackNow); @@ -26,7 +26,7 @@ public void ComputeFromActiveLogs_WhenAllLogsEmpty_ReturnsRoundedFallback() [Fact] public void ComputeFromActiveLogs_WhenMixedEmptyAndPopulatedLogs_UsesPopulatedDataOnly() { - var emptyLog = new EventLogData("Empty", PathType.LogName, []); + var emptyLog = new EventLogData("Empty", LogPathType.Channel, []); var populated = CreateLog( "Populated", new DateTime(2024, 3, 10, 16, 20, 0, DateTimeKind.Utc), @@ -135,6 +135,6 @@ private static EventLogData CreateLog(string name, DateTime newest, DateTime old EventUtils.CreateTestEvent(timeCreated: oldest) }; - return new EventLogData(name, PathType.LogName, events); + return new EventLogData(name, LogPathType.Channel, events); } } diff --git a/tests/Unit/EventLogExpert.UI.Tests/FilterMethodsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/FilterMethodsTests.cs index 48614810..4b549baa 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/FilterMethodsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/FilterMethodsTests.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.UI.Models; using EventLogExpert.UI.Tests.TestUtils; using EventLogExpert.UI.Tests.TestUtils.Constants; @@ -91,123 +91,143 @@ public void AddFilterGroup_WhenSingleGroupName_ShouldAddToRoot() } [Fact] - public void Filter_WhenEventIsNull_ShouldReturnFalse() + public void FilterByDate_WhenDateFilterIsNull_ShouldReturnEvent() { // Arrange - DisplayEventModel? @event = null; - var filters = new List { CreateFilter(Constants.FilterIdEquals100) }; + var @event = EventUtils.CreateTestEvent(100); // Act - var result = @event.Filter(filters); + var result = @event.FilterByDate(null); // Assert - Assert.False(result); + Assert.Same(@event, result); } [Fact] - public void Filter_WhenExcludedFilterDoesNotMatch_ShouldReturnTrue() + public void FilterByDate_WhenEventAfterRange_ShouldReturnNull() { // Arrange - var @event = EventUtils.CreateTestEvent(200); - var filter = CreateFilter(Constants.FilterIdEquals100, true); - var filters = new List { filter }; + var eventTime = DateTime.Now.AddDays(2); + var @event = EventUtils.CreateTestEvent(100, timeCreated: eventTime); + + var dateFilter = new FilterDateModel + { + After = DateTime.Now.AddDays(-1), + Before = DateTime.Now.AddDays(1) + }; // Act - var result = @event.Filter(filters); + var result = @event.FilterByDate(dateFilter); // Assert - Assert.True(result); + Assert.Null(result); } [Fact] - public void Filter_WhenExcludedFilterMatches_ShouldReturnFalse() + public void FilterByDate_WhenEventBeforeRange_ShouldReturnNull() { // Arrange - var @event = EventUtils.CreateTestEvent(100); - var filter = CreateFilter(Constants.FilterIdEquals100, true); - var filters = new List { filter }; + var eventTime = DateTime.Now.AddDays(-2); + var @event = EventUtils.CreateTestEvent(100, timeCreated: eventTime); + + var dateFilter = new FilterDateModel + { + After = DateTime.Now.AddDays(-1), + Before = DateTime.Now.AddDays(1) + }; // Act - var result = @event.Filter(filters); + var result = @event.FilterByDate(dateFilter); // Assert - Assert.False(result); + Assert.Null(result); } [Fact] - public void Filter_WhenFilterDoesNotMatch_ShouldReturnFalse() + public void FilterByDate_WhenEventExactlyAtAfter_ShouldReturnEvent() { // Arrange - var @event = EventUtils.CreateTestEvent(200); - var filters = new List { CreateFilter(Constants.FilterIdEquals100) }; + var boundaryTime = DateTime.Now; + var @event = EventUtils.CreateTestEvent(100, timeCreated: boundaryTime); + + var dateFilter = new FilterDateModel + { + After = boundaryTime, + Before = boundaryTime.AddHours(1) + }; // Act - var result = @event.Filter(filters); + var result = @event.FilterByDate(dateFilter); // Assert - Assert.False(result); + Assert.Same(@event, result); } [Fact] - public void Filter_WhenFilterMatches_ShouldReturnTrue() + public void FilterByDate_WhenEventExactlyAtBefore_ShouldReturnEvent() { // Arrange - var @event = EventUtils.CreateTestEvent(100); - var filters = new List { CreateFilter(Constants.FilterIdEquals100) }; + var boundaryTime = DateTime.Now; + var @event = EventUtils.CreateTestEvent(100, timeCreated: boundaryTime); + + var dateFilter = new FilterDateModel + { + After = boundaryTime.AddHours(-1), + Before = boundaryTime + }; // Act - var result = @event.Filter(filters); + var result = @event.FilterByDate(dateFilter); // Assert - Assert.True(result); + Assert.Same(@event, result); } [Fact] - public void Filter_WhenIncludeAndExcludeFilters_ExcludeTakesPriority() + public void FilterByDate_WhenEventIsNull_ShouldReturnNull() { // Arrange - var @event = EventUtils.CreateTestEvent(100, level: Constants.EventLevelError); - var includeFilter = CreateFilter(Constants.FilterIdEquals100); - var excludeFilter = CreateFilter(Constants.FilterLevelEqualsError, true); - var filters = new List { includeFilter, excludeFilter }; + DisplayEventModel? @event = null; + + var dateFilter = new FilterDateModel + { + After = DateTime.Now.AddDays(-1), + Before = DateTime.Now.AddDays(1) + }; // Act - var result = @event.Filter(filters); + var result = @event.FilterByDate(dateFilter); // Assert - Assert.False(result); + Assert.Null(result); } [Fact] - public void Filter_WhenMultipleFiltersAnyMatch_ShouldReturnTrue() + public void FilterByDate_WhenEventWithinRange_ShouldReturnEvent() { // Arrange - var @event = EventUtils.CreateTestEvent(200); + var eventTime = DateTime.Now; + var @event = EventUtils.CreateTestEvent(100, timeCreated: eventTime); - var filters = new List + var dateFilter = new FilterDateModel { - CreateFilter(Constants.FilterIdEquals100), - CreateFilter(Constants.FilterIdEquals200) + After = eventTime.AddHours(-1), + Before = eventTime.AddHours(1) }; // Act - var result = @event.Filter(filters); + var result = @event.FilterByDate(dateFilter); // Assert - Assert.True(result); + Assert.Same(@event, result); } [Fact] - public void Filter_WhenMultipleFiltersNoneMatch_ShouldReturnFalse() + public void Filter_WhenEventIsNull_ShouldReturnFalse() { // Arrange - var @event = EventUtils.CreateTestEvent(300); - - var filters = new List - { - CreateFilter(Constants.FilterIdEquals100), - CreateFilter(Constants.FilterIdEquals200) - }; + DisplayEventModel? @event = null; + var filters = new List { CreateFilter(Constants.FilterIdEquals100) }; // Act var result = @event.Filter(filters); @@ -217,11 +237,12 @@ public void Filter_WhenMultipleFiltersNoneMatch_ShouldReturnFalse() } [Fact] - public void Filter_WhenNoFilters_ShouldReturnTrue() + public void Filter_WhenExcludedFilterDoesNotMatch_ShouldReturnTrue() { // Arrange - var @event = EventUtils.CreateTestEvent(100); - var filters = new List(); + var @event = EventUtils.CreateTestEvent(200); + var filter = CreateFilter(Constants.FilterIdEquals100, true); + var filters = new List { filter }; // Act var result = @event.Filter(filters); @@ -231,10 +252,10 @@ public void Filter_WhenNoFilters_ShouldReturnTrue() } [Fact] - public void Filter_WhenOnlyExcludedFilters_ShouldReturnTrue() + public void Filter_WhenExcludedFilterMatches_ShouldReturnFalse() { // Arrange - var @event = EventUtils.CreateTestEvent(200); + var @event = EventUtils.CreateTestEvent(100); var filter = CreateFilter(Constants.FilterIdEquals100, true); var filters = new List { filter }; @@ -242,15 +263,15 @@ public void Filter_WhenOnlyExcludedFilters_ShouldReturnTrue() var result = @event.Filter(filters); // Assert - Assert.True(result); + Assert.False(result); } [Fact] - public void Filter_WhenXmlFilterDoesNotMatch_ShouldReturnFalse() + public void Filter_WhenFilterDoesNotMatch_ShouldReturnFalse() { // Arrange - var @event = EventUtils.CreateTestEvent(100); - var filters = new List { CreateFilter(Constants.FilterXmlContainsData) }; + var @event = EventUtils.CreateTestEvent(200); + var filters = new List { CreateFilter(Constants.FilterIdEquals100) }; // Act var result = @event.Filter(filters); @@ -260,135 +281,114 @@ public void Filter_WhenXmlFilterDoesNotMatch_ShouldReturnFalse() } [Fact] - public void FilterByDate_WhenDateFilterIsNull_ShouldReturnEvent() + public void Filter_WhenFilterMatches_ShouldReturnTrue() { // Arrange var @event = EventUtils.CreateTestEvent(100); + var filters = new List { CreateFilter(Constants.FilterIdEquals100) }; // Act - var result = @event.FilterByDate(null); + var result = @event.Filter(filters); // Assert - Assert.Same(@event, result); + Assert.True(result); } [Fact] - public void FilterByDate_WhenEventAfterRange_ShouldReturnNull() + public void Filter_WhenIncludeAndExcludeFilters_ExcludeTakesPriority() { // Arrange - var eventTime = DateTime.Now.AddDays(2); - var @event = EventUtils.CreateTestEvent(100, timeCreated: eventTime); - - var dateFilter = new FilterDateModel - { - After = DateTime.Now.AddDays(-1), - Before = DateTime.Now.AddDays(1) - }; + var @event = EventUtils.CreateTestEvent(100, level: Constants.EventLevelError); + var includeFilter = CreateFilter(Constants.FilterIdEquals100); + var excludeFilter = CreateFilter(Constants.FilterLevelEqualsError, true); + var filters = new List { includeFilter, excludeFilter }; // Act - var result = @event.FilterByDate(dateFilter); + var result = @event.Filter(filters); // Assert - Assert.Null(result); + Assert.False(result); } [Fact] - public void FilterByDate_WhenEventBeforeRange_ShouldReturnNull() + public void Filter_WhenMultipleFiltersAnyMatch_ShouldReturnTrue() { // Arrange - var eventTime = DateTime.Now.AddDays(-2); - var @event = EventUtils.CreateTestEvent(100, timeCreated: eventTime); + var @event = EventUtils.CreateTestEvent(200); - var dateFilter = new FilterDateModel + var filters = new List { - After = DateTime.Now.AddDays(-1), - Before = DateTime.Now.AddDays(1) + CreateFilter(Constants.FilterIdEquals100), + CreateFilter(Constants.FilterIdEquals200) }; // Act - var result = @event.FilterByDate(dateFilter); + var result = @event.Filter(filters); // Assert - Assert.Null(result); + Assert.True(result); } [Fact] - public void FilterByDate_WhenEventExactlyAtAfter_ShouldReturnEvent() + public void Filter_WhenMultipleFiltersNoneMatch_ShouldReturnFalse() { // Arrange - var boundaryTime = DateTime.Now; - var @event = EventUtils.CreateTestEvent(100, timeCreated: boundaryTime); + var @event = EventUtils.CreateTestEvent(300); - var dateFilter = new FilterDateModel + var filters = new List { - After = boundaryTime, - Before = boundaryTime.AddHours(1) + CreateFilter(Constants.FilterIdEquals100), + CreateFilter(Constants.FilterIdEquals200) }; // Act - var result = @event.FilterByDate(dateFilter); + var result = @event.Filter(filters); // Assert - Assert.Same(@event, result); + Assert.False(result); } [Fact] - public void FilterByDate_WhenEventExactlyAtBefore_ShouldReturnEvent() + public void Filter_WhenNoFilters_ShouldReturnTrue() { // Arrange - var boundaryTime = DateTime.Now; - var @event = EventUtils.CreateTestEvent(100, timeCreated: boundaryTime); - - var dateFilter = new FilterDateModel - { - After = boundaryTime.AddHours(-1), - Before = boundaryTime - }; + var @event = EventUtils.CreateTestEvent(100); + var filters = new List(); // Act - var result = @event.FilterByDate(dateFilter); + var result = @event.Filter(filters); // Assert - Assert.Same(@event, result); + Assert.True(result); } [Fact] - public void FilterByDate_WhenEventIsNull_ShouldReturnNull() + public void Filter_WhenOnlyExcludedFilters_ShouldReturnTrue() { // Arrange - DisplayEventModel? @event = null; - - var dateFilter = new FilterDateModel - { - After = DateTime.Now.AddDays(-1), - Before = DateTime.Now.AddDays(1) - }; + var @event = EventUtils.CreateTestEvent(200); + var filter = CreateFilter(Constants.FilterIdEquals100, true); + var filters = new List { filter }; // Act - var result = @event.FilterByDate(dateFilter); + var result = @event.Filter(filters); // Assert - Assert.Null(result); + Assert.True(result); } [Fact] - public void FilterByDate_WhenEventWithinRange_ShouldReturnEvent() + public void Filter_WhenXmlFilterDoesNotMatch_ShouldReturnFalse() { // Arrange - var eventTime = DateTime.Now; - var @event = EventUtils.CreateTestEvent(100, timeCreated: eventTime); - - var dateFilter = new FilterDateModel - { - After = eventTime.AddHours(-1), - Before = eventTime.AddHours(1) - }; + var @event = EventUtils.CreateTestEvent(100); + var filters = new List { CreateFilter(Constants.FilterXmlContainsData) }; // Act - var result = @event.FilterByDate(dateFilter); + var result = @event.Filter(filters); // Assert - Assert.Same(@event, result); + Assert.False(result); } [Fact] @@ -779,43 +779,43 @@ public void SortEvents_WhenEmptyCollection_ShouldReturnEmptyList() } [Fact] - public void SortEvents_WhenNoOrderSpecified_ShouldSortByRecordIdAscending() + public void SortEvents_WhenNoOrderSpecifiedDescending_ShouldSortByRecordIdDescending() { // Arrange var events = new List { - EventUtils.CreateTestEvent(recordId: 3), EventUtils.CreateTestEvent(recordId: 1), + EventUtils.CreateTestEvent(recordId: 3), EventUtils.CreateTestEvent(recordId: 2) }; // Act - var result = events.SortEvents(); + var result = events.SortEvents(isDescending: true); // Assert - Assert.Equal(1L, result[0].RecordId); + Assert.Equal(3L, result[0].RecordId); Assert.Equal(2L, result[1].RecordId); - Assert.Equal(3L, result[2].RecordId); + Assert.Equal(1L, result[2].RecordId); } [Fact] - public void SortEvents_WhenNoOrderSpecifiedDescending_ShouldSortByRecordIdDescending() + public void SortEvents_WhenNoOrderSpecified_ShouldSortByRecordIdAscending() { // Arrange var events = new List { - EventUtils.CreateTestEvent(recordId: 1), EventUtils.CreateTestEvent(recordId: 3), + EventUtils.CreateTestEvent(recordId: 1), EventUtils.CreateTestEvent(recordId: 2) }; // Act - var result = events.SortEvents(isDescending: true); + var result = events.SortEvents(); // Assert - Assert.Equal(3L, result[0].RecordId); + Assert.Equal(1L, result[0].RecordId); Assert.Equal(2L, result[1].RecordId); - Assert.Equal(1L, result[2].RecordId); + Assert.Equal(3L, result[2].RecordId); } [Fact] @@ -1059,45 +1059,45 @@ public void SortEvents_WhenSingleItem_ShouldReturnSingleItem() } [Fact] - public void SortEvents_WhenTiedOnPrimaryKey_ShouldBreakTieByRecordId() + public void SortEvents_WhenTiedOnPrimaryKeyDescending_ShouldBreakTieByRecordIdDescending() { - // Arrange - all events have the same TimeCreated but different RecordIds - var baseTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc); - + // Arrange var events = new List { - EventUtils.CreateTestEvent(timeCreated: baseTime, recordId: 3), - EventUtils.CreateTestEvent(timeCreated: baseTime, recordId: 1), - EventUtils.CreateTestEvent(timeCreated: baseTime, recordId: 2) + EventUtils.CreateTestEvent(100, recordId: 1), + EventUtils.CreateTestEvent(100, recordId: 3), + EventUtils.CreateTestEvent(100, recordId: 2) }; // Act - var result = events.SortEvents(ColumnName.DateAndTime); + var result = events.SortEvents(ColumnName.EventId, true); - // Assert - should be deterministic, ordered by RecordId ascending - Assert.Equal(1L, result[0].RecordId); + // Assert - tied on Id, should fall back to RecordId descending + Assert.Equal(3L, result[0].RecordId); Assert.Equal(2L, result[1].RecordId); - Assert.Equal(3L, result[2].RecordId); + Assert.Equal(1L, result[2].RecordId); } [Fact] - public void SortEvents_WhenTiedOnPrimaryKeyDescending_ShouldBreakTieByRecordIdDescending() + public void SortEvents_WhenTiedOnPrimaryKey_ShouldBreakTieByRecordId() { - // Arrange + // Arrange - all events have the same TimeCreated but different RecordIds + var baseTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var events = new List { - EventUtils.CreateTestEvent(100, recordId: 1), - EventUtils.CreateTestEvent(100, recordId: 3), - EventUtils.CreateTestEvent(100, recordId: 2) + EventUtils.CreateTestEvent(timeCreated: baseTime, recordId: 3), + EventUtils.CreateTestEvent(timeCreated: baseTime, recordId: 1), + EventUtils.CreateTestEvent(timeCreated: baseTime, recordId: 2) }; // Act - var result = events.SortEvents(ColumnName.EventId, true); + var result = events.SortEvents(ColumnName.DateAndTime); - // Assert - tied on Id, should fall back to RecordId descending - Assert.Equal(3L, result[0].RecordId); + // Assert - should be deterministic, ordered by RecordId ascending + Assert.Equal(1L, result[0].RecordId); Assert.Equal(2L, result[1].RecordId); - Assert.Equal(1L, result[2].RecordId); + Assert.Equal(3L, result[2].RecordId); } private static FilterModel CreateFilter(string expression, bool isExcluded = false) => diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/BannerServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/BannerServiceTests.cs index 95025611..54ff4843 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/BannerServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/BannerServiceTests.cs @@ -444,7 +444,7 @@ public void RaiseStateChanged_WhenSubscriberThrows_StillFiresLaterSubscribersAnd // Assert Assert.True(secondCalled); - traceLogger.Received(1).Warn(Arg.Any()); + traceLogger.Received(1).Warning(Arg.Any()); } [Fact] diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/DatabaseServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/DatabaseServiceTests.cs index d3580b67..32b88659 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/DatabaseServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/DatabaseServiceTests.cs @@ -174,7 +174,7 @@ public async Task ClassifyEntriesAsync_WhenOneEntryFails_ShouldQuarantineAsClass var lockedPath = Path.Combine(databasePath, "locked.db"); DatabaseSeedUtils.SeedV3Schema(lockedPath); - // Hold an exclusive lock on the second file so EventProviderDbContext cannot open it. + // Hold an exclusive lock on the second file so ProviderDbContext cannot open it. // The classification pass must mark the locked entry as ClassificationFailed so the // resolver pipeline cannot consume it later (a Ready status would crash IEventResolver // when it tried to open the same locked file). @@ -258,48 +258,51 @@ public async Task ClassifyEntriesAsync_WhenV3BakAppearsBetweenClassifications_Sh } [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_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_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); @@ -307,18 +310,16 @@ 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] - 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); @@ -326,7 +327,6 @@ 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] @@ -647,6 +647,25 @@ 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() { @@ -707,25 +726,6 @@ 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() { @@ -1207,7 +1207,7 @@ public async Task InitialClassificationTask_NeverFaults_EvenWhenAllEntriesThrow( DatabaseSeedUtils.SeedV3Schema(db1Path); DatabaseSeedUtils.SeedV3Schema(db2Path); - // Hold exclusive locks on every DB file so EventProviderDbContext cannot open any of them. + // Hold exclusive locks on every DB file so ProviderDbContext cannot open any of them. // The per-entry catch must turn each failure into ClassificationFailed and the outer // wrapper must keep the exposed task in RanToCompletion. using var handle1 = new FileStream(db1Path, FileMode.Open, FileAccess.ReadWrite, FileShare.None); @@ -1256,7 +1256,7 @@ public async Task InitialClassificationTask_WhenLoggerThrowsOnWarn_StillComplete // Logger throws on every Warn — simulates debug.log being locked. Without SafeLog the // per-entry catch would propagate, faulting the worker before statuses are applied. var throwingLogger = Substitute.For(); - throwingLogger.When(logger => logger.Warn(Arg.Any())) + throwingLogger.When(logger => logger.Warning(Arg.Any())) .Do(_ => throw new IOException("simulated log file lock")); var service = CreateDatabaseService(traceLogger: throwingLogger); @@ -1283,7 +1283,7 @@ public async Task InitialClassificationTask_WhenSubscriberAndLoggerBothThrow_Sti using var subscriberAttached = new ManualResetEventSlim(false); var throwingLogger = Substitute.For(); - throwingLogger.When(logger => logger.Warn(Arg.Any())) + throwingLogger.When(logger => logger.Warning(Arg.Any())) .Do(_ => { // Worker blocks here until subscriberAttached.Set() below; ensures the @@ -1306,7 +1306,7 @@ public async Task InitialClassificationTask_WhenSubscriberAndLoggerBothThrow_Sti Assert.Equal(TaskStatus.RanToCompletion, service.InitialClassificationTask.Status); // Per-entry SafeLog (ClassificationFailed) plus wrapper SafeLog (subscriber fault) — both // fired and both throws were swallowed for the task to RanToCompletion. - throwingLogger.Received(2).Warn(Arg.Any()); + throwingLogger.Received(2).Warning(Arg.Any()); } [Fact] diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLogServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLogServiceTests.cs index 19c29947..e64db784 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLogServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLogServiceTests.cs @@ -69,18 +69,18 @@ public async Task ClearAsync_WhenSecondInstanceHoldsWriter_ShouldNotThrowFileLoc var mockSettingsService = CreateMockSettingsService(LogLevel.Information); using var firstInstance = new DebugLogService(fileLocationOptions, mockSettingsService); - firstInstance.Info($"first instance line"); + firstInstance.Information($"first instance line"); using var secondInstance = new DebugLogService(fileLocationOptions, mockSettingsService); - secondInstance.Info($"second instance line"); + secondInstance.Information($"second instance line"); // Act + Assert - clearing from one instance must not fail because the other holds the writer. var exception = await Record.ExceptionAsync(() => firstInstance.ClearAsync()); Assert.Null(exception); // Both instances must write cleanly post-clear; no NUL padding from stale positions. - secondInstance.Info($"after clear from second"); - firstInstance.Info($"after clear from first"); + secondInstance.Information($"after clear from second"); + firstInstance.Information($"after clear from first"); var bytes = ReadLogFileBytes(); Assert.DoesNotContain((byte)0, bytes); @@ -139,7 +139,7 @@ public void Constructor_WhenLogFileUnderMaxSize_ShouldNotDeleteFile() // Act using var debugLogService = new DebugLogService(fileLocationOptions, mockSettingsService); - debugLogService.Info($"{Constants.DebugLogNewMessage}"); + debugLogService.Information($"{Constants.DebugLogNewMessage}"); // Assert var content = ReadLogFile(); @@ -330,6 +330,37 @@ 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() { @@ -340,7 +371,7 @@ public void Trace_ShouldIncludeThreadId() using var debugLogService = new DebugLogService(fileLocationOptions, mockSettingsService); // Act - debugLogService.Info($"{Constants.DebugLogTestMessage}"); + debugLogService.Information($"{Constants.DebugLogTestMessage}"); // Assert var content = ReadLogFile(); @@ -358,7 +389,7 @@ public void Trace_ShouldIncludeTimestamp() using var debugLogService = new DebugLogService(fileLocationOptions, mockSettingsService); // Act - debugLogService.Info($"{Constants.DebugLogTestMessage}"); + debugLogService.Information($"{Constants.DebugLogTestMessage}"); // Assert var content = ReadLogFile(); @@ -391,7 +422,7 @@ public async Task Trace_WhenCalledFromMultipleThreadsConcurrently_ShouldProduceO for (var writeIndex = 0; writeIndex < WritesPerWriter; writeIndex++) { - debugLogService.Info($"writer-{writerIndex}-msg-{writeIndex}"); + debugLogService.Information($"writer-{writerIndex}-msg-{writeIndex}"); } }, TestContext.Current.CancellationToken)) .ToArray(); @@ -450,9 +481,9 @@ public void Trace_WhenCalledMultipleTimes_ShouldAppendToLogFile() using var debugLogService = new DebugLogService(fileLocationOptions, mockSettingsService); // Act - debugLogService.Info($"{Constants.DebugLogFirstMessage}"); - debugLogService.Info($"{Constants.DebugLogSecondMessage}"); - debugLogService.Info($"{Constants.DebugLogThirdMessage}"); + debugLogService.Information($"{Constants.DebugLogFirstMessage}"); + debugLogService.Information($"{Constants.DebugLogSecondMessage}"); + debugLogService.Information($"{Constants.DebugLogThirdMessage}"); // Assert var content = ReadLogFile(); @@ -472,16 +503,16 @@ public void Trace_WhenLogLevelChangedAtRuntime_ShouldRespectNewLevel() mockSettingsService); // Act - write at Information (should succeed) - debugLogService.Info($"{Constants.DebugLogFirstMessage}"); + debugLogService.Information($"{Constants.DebugLogFirstMessage}"); // Change level to Warning at runtime setLogLevel(LogLevel.Warning); // Write at Information (should be filtered now) - debugLogService.Info($"{Constants.DebugLogSecondMessage}"); + debugLogService.Information($"{Constants.DebugLogSecondMessage}"); // Write at Warning (should succeed) - debugLogService.Warn($"{Constants.DebugLogThirdMessage}"); + debugLogService.Warning($"{Constants.DebugLogThirdMessage}"); // Assert var content = ReadLogFile(); @@ -541,7 +572,7 @@ public void Trace_WhenLogLevelIsBelowThreshold_ShouldNotWriteToLogFile() using var debugLogService = new DebugLogService(fileLocationOptions, mockSettingsService); // Act - debugLogService.Info($"{Constants.DebugLogTestMessage}"); + debugLogService.Information($"{Constants.DebugLogTestMessage}"); // Assert Assert.False(File.Exists(_testLogPath) && ReadLogFile().Contains(Constants.DebugLogTestMessage)); @@ -557,7 +588,7 @@ public void Trace_WhenLogLevelIsMet_ShouldWriteToLogFile() using var debugLogService = new DebugLogService(fileLocationOptions, mockSettingsService); // Act - debugLogService.Info($"{Constants.DebugLogTestMessage}"); + debugLogService.Information($"{Constants.DebugLogTestMessage}"); // Assert var content = ReadLogFile(); @@ -575,7 +606,7 @@ public void Trace_WithDefaultLogLevel_ShouldUseInformation() using var debugLogService = new DebugLogService(fileLocationOptions, mockSettingsService); // Act - debugLogService.Info($"{Constants.DebugLogDefaultLevelMessage}"); + debugLogService.Information($"{Constants.DebugLogDefaultLevelMessage}"); // Assert var content = ReadLogFile(); @@ -583,37 +614,6 @@ 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.Warn($"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() { @@ -622,12 +622,12 @@ public void WriteTrace_WhenSecondInstanceWritesToSameFile_ShouldNotThrowFileLock var mockSettingsService = CreateMockSettingsService(LogLevel.Information); using var firstInstance = new DebugLogService(fileLocationOptions, mockSettingsService); - firstInstance.Info($"first instance line"); + firstInstance.Information($"first instance line"); using var secondInstance = new DebugLogService(fileLocationOptions, mockSettingsService); // Act + Assert - var exception = Record.Exception(() => secondInstance.Info($"second instance line")); + var exception = Record.Exception(() => secondInstance.Information($"second instance line")); Assert.Null(exception); } @@ -662,8 +662,8 @@ private static void TraceAtLevel(DebugLogService service, string message, LogLev { case LogLevel.Trace: service.Trace($"{message}"); break; case LogLevel.Debug: service.Debug($"{message}"); break; - case LogLevel.Information: service.Info($"{message}"); break; - case LogLevel.Warning: service.Warn($"{message}"); break; + case LogLevel.Information: service.Information($"{message}"); break; + case LogLevel.Warning: service.Warning($"{message}"); break; case LogLevel.Error: service.Error($"{message}"); break; case LogLevel.Critical: service.Critical($"{message}"); break; default: throw new ArgumentOutOfRangeException(nameof(level)); diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.cs index 92a18607..cde053f1 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; -using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Common.Channels; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.UI.Models; using EventLogExpert.UI.Services; using EventLogExpert.UI.Tests.TestUtils; @@ -17,6 +17,24 @@ public sealed class FilterCategoryItemsCacheTests : IDisposable public void Dispose() => FilterCategoryItemsCache.Clear(); + [Fact] + public void GetItems_DifferentSnapshot_RecomputesValues() + { + var snapshotA = ImmutableDictionary.Empty.Add( + Constants.LogNameTestLog, + new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, [EventUtils.CreateTestEvent(id: 100)])); + + var snapshotB = snapshotA.Add( + Constants.LogNameLog2, + new EventLogData(Constants.LogNameLog2, LogPathType.Channel, [EventUtils.CreateTestEvent(id: 200)])); + + var idsA = FilterCategoryItemsCache.GetItems(snapshotA, FilterCategory.Id); + var idsB = FilterCategoryItemsCache.GetItems(snapshotB, FilterCategory.Id); + + Assert.Equal(["100"], idsA); + Assert.Equal(["100", "200"], idsB); + } + [Fact] public void GetItems_LevelCategory_ReturnsAllSeverityLevelNames() { @@ -38,7 +56,7 @@ public void GetItems_LogDerivedCategory_ReturnsDistinctSortedValues() EventUtils.CreateTestEvent(id: 300, source: "Charlie") }; - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, events); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, events); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); var ids = FilterCategoryItemsCache.GetItems(activeLogs, FilterCategory.Id); @@ -53,7 +71,7 @@ public void GetItems_SameSnapshotAndCategory_ReturnsCachedInstance() { var logData = new EventLogData( Constants.LogNameTestLog, - PathType.LogName, + LogPathType.Channel, [EventUtils.CreateTestEvent(id: 100)]); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); @@ -64,30 +82,12 @@ public void GetItems_SameSnapshotAndCategory_ReturnsCachedInstance() Assert.True(first == second, "ImmutableArray instance should be reused for the same snapshot/category."); } - [Fact] - public void GetItems_DifferentSnapshot_RecomputesValues() - { - var snapshotA = ImmutableDictionary.Empty.Add( - Constants.LogNameTestLog, - new EventLogData(Constants.LogNameTestLog, PathType.LogName, [EventUtils.CreateTestEvent(id: 100)])); - - var snapshotB = snapshotA.Add( - Constants.LogNameLog2, - new EventLogData(Constants.LogNameLog2, PathType.LogName, [EventUtils.CreateTestEvent(id: 200)])); - - var idsA = FilterCategoryItemsCache.GetItems(snapshotA, FilterCategory.Id); - var idsB = FilterCategoryItemsCache.GetItems(snapshotB, FilterCategory.Id); - - Assert.Equal(["100"], idsA); - Assert.Equal(["100", "200"], idsB); - } - [Fact] public void GetItems_UnsupportedCategory_ReturnsEmpty() { var activeLogs = ImmutableDictionary.Empty.Add( Constants.LogNameTestLog, - new EventLogData(Constants.LogNameTestLog, PathType.LogName, [EventUtils.CreateTestEvent()])); + new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, [EventUtils.CreateTestEvent()])); var items = FilterCategoryItemsCache.GetItems(activeLogs, FilterCategory.Description); diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/FilterServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/FilterServiceTests.cs index 06162bb6..4a15a415 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/FilterServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/FilterServiceTests.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; -using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Common.Channels; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.UI.Models; using EventLogExpert.UI.Services; using EventLogExpert.UI.Tests.TestUtils; @@ -59,8 +59,8 @@ public void FilterActiveLogs_WhenMultipleLargeLogs_ShouldMatchSequentialResult() var logData = new List { - new("Log1", PathType.LogName, log1Events), - new("Log2", PathType.LogName, log2Events) + new("Log1", LogPathType.Channel, log1Events), + new("Log2", LogPathType.Channel, log2Events) }; var expectedLog1 = log1Events.Where(e => e.TimeCreated >= cutoff && e.TimeCreated <= baseTime.AddMinutes(20_000)).ToList(); @@ -78,27 +78,19 @@ public void FilterActiveLogs_WhenMultipleLargeLogs_ShouldMatchSequentialResult() } [Fact] - public void FilterActiveLogs_WhenMultipleLogs_ShouldFilterEachLog() + public void FilterActiveLogs_WhenMultipleLogsButFilteringDisabled_ShouldReturnAllEventsPerLog() { - // Arrange + // Arrange — IsFilteringEnabled short-circuit must stay engaged even with many logs to + // avoid spinning up parallel work for a no-op filter. var filterService = CreateFilterService(); - var log1Events = new List - { - EventUtils.CreateTestEvent(100, level: Constants.EventLevelError), - EventUtils.CreateTestEvent(200, level: Constants.EventLevelInformation) - }; - - var log2Events = new List - { - EventUtils.CreateTestEvent(100, level: Constants.EventLevelError), - EventUtils.CreateTestEvent(300, level: Constants.EventLevelError) - }; + var log1Events = Enumerable.Range(0, 6_000).Select(i => EventUtils.CreateTestEvent(i)).ToList(); + var log2Events = Enumerable.Range(0, 6_000).Select(i => EventUtils.CreateTestEvent(i)).ToList(); var logData = new List { - new("Log1", PathType.LogName, log1Events), - new("Log2", PathType.LogName, log2Events) + new("Log1", LogPathType.Channel, log1Events), + new("Log2", LogPathType.Channel, log2Events) }; var eventFilter = new EventFilter(null, []); @@ -107,23 +99,32 @@ public void FilterActiveLogs_WhenMultipleLogs_ShouldFilterEachLog() var result = filterService.FilterActiveLogs(logData, eventFilter); // Assert - Assert.Equal(2, result.Count); + Assert.Equal(log1Events.Count, result[logData[0].Id].Count); + Assert.Equal(log2Events.Count, result[logData[1].Id].Count); } [Fact] - public void FilterActiveLogs_WhenMultipleLogsButFilteringDisabled_ShouldReturnAllEventsPerLog() + public void FilterActiveLogs_WhenMultipleLogs_ShouldFilterEachLog() { - // Arrange — IsFilteringEnabled short-circuit must stay engaged even with many logs to - // avoid spinning up parallel work for a no-op filter. + // Arrange var filterService = CreateFilterService(); - var log1Events = Enumerable.Range(0, 6_000).Select(i => EventUtils.CreateTestEvent(i)).ToList(); - var log2Events = Enumerable.Range(0, 6_000).Select(i => EventUtils.CreateTestEvent(i)).ToList(); + var log1Events = new List + { + EventUtils.CreateTestEvent(100, level: Constants.EventLevelError), + EventUtils.CreateTestEvent(200, level: Constants.EventLevelInformation) + }; + + var log2Events = new List + { + EventUtils.CreateTestEvent(100, level: Constants.EventLevelError), + EventUtils.CreateTestEvent(300, level: Constants.EventLevelError) + }; var logData = new List { - new("Log1", PathType.LogName, log1Events), - new("Log2", PathType.LogName, log2Events) + new("Log1", LogPathType.Channel, log1Events), + new("Log2", LogPathType.Channel, log2Events) }; var eventFilter = new EventFilter(null, []); @@ -132,8 +133,7 @@ public void FilterActiveLogs_WhenMultipleLogsButFilteringDisabled_ShouldReturnAl var result = filterService.FilterActiveLogs(logData, eventFilter); // Assert - Assert.Equal(log1Events.Count, result[logData[0].Id].Count); - Assert.Equal(log2Events.Count, result[logData[1].Id].Count); + Assert.Equal(2, result.Count); } [Fact] @@ -150,7 +150,7 @@ public void FilterActiveLogs_WhenNoFiltersEnabled_ShouldReturnAllEvents() var logData = new List { - new("TestLog", PathType.LogName, events) + new("TestLog", LogPathType.Channel, events) }; var eventFilter = new EventFilter(null, []); @@ -251,6 +251,94 @@ public void GetFilteredEvents_WhenNoFiltersEnabled_ShouldReturnAllEvents() Assert.Equal(3, result.Count); } + [Theory] + [InlineData("Id == 100")] + [InlineData("Level == \"Error\"")] + [InlineData("Source.Contains(\"Test\", StringComparison.OrdinalIgnoreCase)")] + [InlineData("Id > 100 && Level == \"Error\"")] + [InlineData("Id == 100 || Id == 200")] + public void TryParseExpression_WhenCommonExpressions_ShouldReturnTrue(string expression) + { + // Arrange + var filterService = CreateFilterService(); + + // Act + var result = filterService.TryParseExpression(expression, out var error); + + // Assert + Assert.True(result); + Assert.Empty(error); + } + + [Fact] + public void TryParseExpression_WhenEmptyExpression_ShouldReturnFalse() + { + // Arrange + var filterService = CreateFilterService(); + + // Act + var result = filterService.TryParseExpression(string.Empty, out var error); + + // Assert + Assert.False(result); + } + + [Fact] + public void TryParseExpression_WhenInvalidExpression_ShouldReturnFalseWithError() + { + // Arrange + var filterService = CreateFilterService(); + + // Act + var result = filterService.TryParseExpression(Constants.FilterInvalidProperty, out var error); + + // Assert + Assert.False(result); + Assert.NotEmpty(error); + } + + [Fact] + public void TryParseExpression_WhenNullExpression_ShouldReturnFalse() + { + // Arrange + var filterService = CreateFilterService(); + + // Act + var result = filterService.TryParseExpression(null, out var error); + + // Assert + Assert.False(result); + } + + [Fact] + public void TryParseExpression_WhenValidExpression_ShouldReturnTrue() + { + // Arrange + var filterService = CreateFilterService(); + + // Act + var result = filterService.TryParseExpression(Constants.FilterIdEquals100, out var error); + + // Assert + Assert.True(result); + Assert.Empty(error); + } + + [Fact] + public void TryParseExpression_WhenXmlExpression_ShouldReturnTrue() + { + // Arrange + var filterService = CreateFilterService(); + + // Act + var result = filterService.TryParseExpression("Xml.Contains(\"test\", StringComparison.OrdinalIgnoreCase)", + out var error); + + // Assert + Assert.True(result); + Assert.Empty(error); + } + [Fact] public void TryParse_WhenCategoryIsXml_ShouldReturnTrue() { @@ -461,7 +549,7 @@ public void TryParse_WhenQuotesInValue_ShouldEscapeQuotes() } [Fact] - public void TryParse_WhenSubFiltersExist_ShouldAppendSubFilterComparison() + public void TryParse_WhenSubFilterWithCompareAny_ShouldUseOrOperator() { // Arrange var filterService = CreateFilterService(); @@ -470,7 +558,7 @@ public void TryParse_WhenSubFiltersExist_ShouldAppendSubFilterComparison() [ new SubFilter( new FilterData { Category = FilterCategory.Level, Evaluator = FilterEvaluator.Equals, Value = "Error" }, - JoinWithAny: false) + JoinWithAny: true) ]); // Act @@ -478,12 +566,11 @@ public void TryParse_WhenSubFiltersExist_ShouldAppendSubFilterComparison() // Assert Assert.True(result); - Assert.Contains("Id == \"100\"", comparison); - Assert.Contains("Level == \"Error\"", comparison); + Assert.Contains(" || ", comparison); } [Fact] - public void TryParse_WhenSubFilterWithCompareAny_ShouldUseOrOperator() + public void TryParse_WhenSubFilterWithoutCompareAny_ShouldUseAndOperator() { // Arrange var filterService = CreateFilterService(); @@ -492,7 +579,7 @@ public void TryParse_WhenSubFilterWithCompareAny_ShouldUseOrOperator() [ new SubFilter( new FilterData { Category = FilterCategory.Level, Evaluator = FilterEvaluator.Equals, Value = "Error" }, - JoinWithAny: true) + JoinWithAny: false) ]); // Act @@ -500,11 +587,11 @@ public void TryParse_WhenSubFilterWithCompareAny_ShouldUseOrOperator() // Assert Assert.True(result); - Assert.Contains(" || ", comparison); + Assert.Contains(" && ", comparison); } [Fact] - public void TryParse_WhenSubFilterWithoutCompareAny_ShouldUseAndOperator() + public void TryParse_WhenSubFiltersExist_ShouldAppendSubFilterComparison() { // Arrange var filterService = CreateFilterService(); @@ -521,7 +608,8 @@ public void TryParse_WhenSubFilterWithoutCompareAny_ShouldUseAndOperator() // Assert Assert.True(result); - Assert.Contains(" && ", comparison); + Assert.Contains("Id == \"100\"", comparison); + Assert.Contains("Level == \"Error\"", comparison); } [Fact] @@ -722,94 +810,6 @@ public void TryParse_WithBasicFilter_WhenSubFiltersPresent_ShouldUseExactJoinAnd Assert.Equal(expected, comparison); } - [Theory] - [InlineData("Id == 100")] - [InlineData("Level == \"Error\"")] - [InlineData("Source.Contains(\"Test\", StringComparison.OrdinalIgnoreCase)")] - [InlineData("Id > 100 && Level == \"Error\"")] - [InlineData("Id == 100 || Id == 200")] - public void TryParseExpression_WhenCommonExpressions_ShouldReturnTrue(string expression) - { - // Arrange - var filterService = CreateFilterService(); - - // Act - var result = filterService.TryParseExpression(expression, out var error); - - // Assert - Assert.True(result); - Assert.Empty(error); - } - - [Fact] - public void TryParseExpression_WhenEmptyExpression_ShouldReturnFalse() - { - // Arrange - var filterService = CreateFilterService(); - - // Act - var result = filterService.TryParseExpression(string.Empty, out var error); - - // Assert - Assert.False(result); - } - - [Fact] - public void TryParseExpression_WhenInvalidExpression_ShouldReturnFalseWithError() - { - // Arrange - var filterService = CreateFilterService(); - - // Act - var result = filterService.TryParseExpression(Constants.FilterInvalidProperty, out var error); - - // Assert - Assert.False(result); - Assert.NotEmpty(error); - } - - [Fact] - public void TryParseExpression_WhenNullExpression_ShouldReturnFalse() - { - // Arrange - var filterService = CreateFilterService(); - - // Act - var result = filterService.TryParseExpression(null, out var error); - - // Assert - Assert.False(result); - } - - [Fact] - public void TryParseExpression_WhenValidExpression_ShouldReturnTrue() - { - // Arrange - var filterService = CreateFilterService(); - - // Act - var result = filterService.TryParseExpression(Constants.FilterIdEquals100, out var error); - - // Assert - Assert.True(result); - Assert.Empty(error); - } - - [Fact] - public void TryParseExpression_WhenXmlExpression_ShouldReturnTrue() - { - // Arrange - var filterService = CreateFilterService(); - - // Act - var result = filterService.TryParseExpression("Xml.Contains(\"test\", StringComparison.OrdinalIgnoreCase)", - out var error); - - // Assert - Assert.True(result); - Assert.Empty(error); - } - private static BasicFilter CreateBasicFilter( FilterCategory category, FilterEvaluator evaluator, diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogEffectsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogEffectsTests.cs index 00178deb..ac0522d8 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogEffectsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogEffectsTests.cs @@ -1,10 +1,11 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventResolvers; +using EventLogExpert.Eventing.Common.Channels; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.Eventing.Logging; -using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Resolvers; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.EventLog; @@ -30,7 +31,7 @@ public async Task HandleAddEvent_WhenBufferReachesMaxEvents_ShouldSetFullFlag() .Select(i => EventUtils.CreateTestEvent(i, logName: Constants.LogNameTestLog)) .ToList(); - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); var (effects, mockDispatcher) = CreateEffects(false, activeLogs, existingBuffer); @@ -50,7 +51,7 @@ public async Task HandleAddEvent_WhenBufferReachesMaxEvents_ShouldSetFullFlag() public async Task HandleAddEvent_WhenContinuouslyUpdateFalse_ShouldBufferEvent() { // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); var (effects, mockDispatcher) = CreateEffects( @@ -72,7 +73,7 @@ public async Task HandleAddEvent_WhenContinuouslyUpdateFalse_ShouldBufferEvent() public async Task HandleAddEvent_WhenContinuouslyUpdateTrue_AndEventFilteredOut_ShouldNotDispatchAppend() { // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); var (effects, mockDispatcher, _, _, mockFilterService) = @@ -98,7 +99,7 @@ public async Task HandleAddEvent_WhenContinuouslyUpdateTrue_AndEventFilteredOut_ public async Task HandleAddEvent_WhenContinuouslyUpdateTrue_ShouldDispatchSuccessAndUpdate() { // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); var (effects, mockDispatcher) = CreateEffects( @@ -261,7 +262,7 @@ public async Task HandleCloseLog_ShouldClearResolvedXmlForLog() await effects.HandleCloseLog(action, mockDispatcher); // Assert - mockXmlResolver.Received(1).ClearLog(Constants.LogNameTestLog); + mockXmlResolver.Received(1).ClearXmlCacheForLog(Constants.LogNameTestLog); } [Fact] @@ -301,7 +302,7 @@ public async Task HandleCloseLog_WhenLastLog_ShouldClearResolverCache() public async Task HandleCloseLog_WhenOtherLogsRemain_ShouldNotClearResolverCache() { // Arrange — state still has another active log - var logData = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty .Add(Constants.LogNameLog1, logData); @@ -325,7 +326,7 @@ public async Task HandleLoadEvents_ShouldFilterAndDispatchUpdateTable() EventUtils.CreateTestEvent(200, level: Constants.EventLevelInformation) ); - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var (effects, mockDispatcher, _, _, mockFilterService) = CreateEffectsWithServices(); var action = new EventLogAction.LoadEvents(logData, events); @@ -348,7 +349,7 @@ public async Task HandleLoadNewEvents_ShouldProcessBufferAndDispatchActions() EventUtils.CreateTestEvent(200, logName: Constants.LogNameTestLog) }; - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); var (effects, mockDispatcher) = CreateEffects( @@ -379,7 +380,7 @@ public async Task HandleLoadNewEvents_WhenAllEventsFiltered_ShouldNotDispatchApp EventUtils.CreateTestEvent(100, logName: Constants.LogNameTestLog) }; - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); var (effects, mockDispatcher, _, _, mockFilterService) = @@ -410,8 +411,8 @@ public async Task HandleLoadNewEvents_WhenBufferSpansMultipleLogs_ShouldGroupInt EventUtils.CreateTestEvent(300) with { OwningLog = Constants.LogNameApplication } }; - var applicationLog = new EventLogData(Constants.LogNameApplication, PathType.LogName, []); - var testLog = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var applicationLog = new EventLogData(Constants.LogNameApplication, LogPathType.Channel, []); + var testLog = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty .Add(Constants.LogNameApplication, applicationLog) @@ -446,7 +447,7 @@ public async Task HandleOpenLog_AwaitsInitialClassificationTask_BeforeResolverCo // is NOT looked up until classification completes. Determinism comes from the await // semantics: the IServiceProvider mock cannot be invoked while the await is parked // on an incomplete task, so an immediate post-call assertion is sufficient. - var logData = new EventLogData(Constants.LogNameApplication, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameApplication, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameApplication, logData); var classificationTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -456,7 +457,7 @@ public async Task HandleOpenLog_AwaitsInitialClassificationTask_BeforeResolverCo mockDatabaseService.InitialClassificationTask.Returns(classificationTcs.Task); - var action = new EventLogAction.OpenLog(Constants.LogNameApplication, PathType.LogName); + var action = new EventLogAction.OpenLog(Constants.LogNameApplication, LogPathType.Channel); // Act 1 — start the open; await yields back at InitialClassificationTask. var openTask = effects.HandleOpenLog(action, mockDispatcher); @@ -480,7 +481,7 @@ public async Task HandleOpenLog_LogClosedDuringClassificationAwait_DoesNotDispat // classification await. After the await releases, HandleOpenLog must bail BEFORE // calling LoadLogAsync — otherwise LoadLogAsync's AddTable dispatch would resurrect // a table entry the user already dismissed, leaving an orphan in EventTableState. - var logData = new EventLogData(Constants.LogNameApplication, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameApplication, LogPathType.Channel, []); var initialActiveLogs = ImmutableDictionary.Empty.Add(Constants.LogNameApplication, logData); var classificationTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -520,7 +521,7 @@ public async Task HandleOpenLog_LogClosedDuringClassificationAwait_DoesNotDispat Substitute.For()); var mockDispatcher = Substitute.For(); - var action = new EventLogAction.OpenLog(Constants.LogNameApplication, PathType.LogName); + var action = new EventLogAction.OpenLog(Constants.LogNameApplication, LogPathType.Channel); // Act 1 — start the open; await yields back at InitialClassificationTask. var openTask = effects.HandleOpenLog(action, mockDispatcher); @@ -549,7 +550,7 @@ public async Task HandleOpenLog_ResolverThrows_CallsReportCritical_DoesNotPropag // Arrange — resolver factory throws (e.g., DI graph misconfiguration). HandleOpenLog // must surface this as a Reload-tier banner via IBannerService.ReportCritical and // return cleanly instead of letting the exception escape the effect. - var logData = new EventLogData(Constants.LogNameApplication, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameApplication, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameApplication, logData); var (effects, mockDispatcher, mockServiceProvider, mockBannerService, _) = @@ -558,7 +559,7 @@ public async Task HandleOpenLog_ResolverThrows_CallsReportCritical_DoesNotPropag var thrown = new InvalidOperationException("resolver factory failed"); mockServiceProvider.When(p => p.GetService(typeof(IEventResolver))).Do(_ => throw thrown); - var action = new EventLogAction.OpenLog(Constants.LogNameApplication, PathType.LogName); + var action = new EventLogAction.OpenLog(Constants.LogNameApplication, LogPathType.Channel); // Act — must not throw. await effects.HandleOpenLog(action, mockDispatcher); @@ -573,7 +574,7 @@ public async Task HandleOpenLog_ResolverThrows_CallsReportCritical_DoesNotPropag public async Task HandleOpenLog_WhenCancelled_ShouldDispatchCloseAndClearStatus() { // Arrange - var logData = new EventLogData(Constants.LogNameApplication, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameApplication, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameApplication, logData); var cts = new CancellationTokenSource(); @@ -583,7 +584,7 @@ public async Task HandleOpenLog_WhenCancelled_ShouldDispatchCloseAndClearStatus( activeLogs: activeLogs, hasEventResolver: true); - var action = new EventLogAction.OpenLog(Constants.LogNameApplication, PathType.LogName, cts.Token); + var action = new EventLogAction.OpenLog(Constants.LogNameApplication, LogPathType.Channel, cts.Token); // Act await effects.HandleOpenLog(action, mockDispatcher); @@ -598,7 +599,7 @@ public async Task HandleOpenLog_WhenLogNotInActiveLogs_ShouldDispatchError() { // Arrange var (effects, mockDispatcher) = CreateEffects(hasEventResolver: true); - var action = new EventLogAction.OpenLog(Constants.LogNameTestLog, PathType.LogName); + var action = new EventLogAction.OpenLog(Constants.LogNameTestLog, LogPathType.Channel); // Act await effects.HandleOpenLog(action, mockDispatcher); @@ -612,14 +613,14 @@ public async Task HandleOpenLog_WhenLogNotInActiveLogs_ShouldDispatchError() public async Task HandleOpenLog_WhenNoEventResolver_ShouldDispatchError() { // Arrange - var logData = new EventLogData(Constants.LogNameApplication, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameApplication, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameApplication, logData); var (effects, mockDispatcher) = CreateEffects( activeLogs: activeLogs, hasEventResolver: false); - var action = new EventLogAction.OpenLog(Constants.LogNameApplication, PathType.LogName); + var action = new EventLogAction.OpenLog(Constants.LogNameApplication, LogPathType.Channel); // Act await effects.HandleOpenLog(action, mockDispatcher); @@ -657,7 +658,7 @@ public async Task HandleSetContinuouslyUpdate_WhenTrue_ShouldProcessBuffer() EventUtils.CreateTestEvent(100, logName: Constants.LogNameTestLog) }; - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); var (effects, mockDispatcher) = CreateEffects( @@ -678,7 +679,7 @@ public async Task HandleSetContinuouslyUpdate_WhenTrue_ShouldProcessBuffer() [Fact] public async Task HandleSetFilters_FilterBranch_ShouldDispatchSetIsLoadingTrueThenFalse() { - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); var (effects, mockDispatcher, _, _, _) = CreateEffectsWithServices(activeLogs: activeLogs); @@ -699,7 +700,7 @@ public async Task HandleSetFilters_FilterBranch_ShouldDispatchSetIsLoadingTrueTh [Fact] public async Task HandleSetFilters_FilterBranch_WhenFilterServiceThrows_ShouldStillClearLoading() { - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); var (effects, mockDispatcher, _, _, mockFilterService) = @@ -727,7 +728,7 @@ public async Task HandleSetFilters_FilterBranch_WhenLogClosedDuringFilter_Should // post-filter dispatch must omit the closed log's slice. (The reducer also skips // unknown log ids, but checking at the effect keeps the dispatch minimal.) var snapshotEvents = new List { EventUtils.CreateTestEvent(100) }; - var snapshotData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, snapshotEvents); + var snapshotData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, snapshotEvents); var snapshotState = new EventLogState { @@ -776,7 +777,7 @@ public async Task HandleSetFilters_FilterBranch_WhenLogEventsChangeDuringFilter_ // changes). The new filter must be re-applied to the post-mutation rows in a single retry // pass so the user sees the filter applied to the updated row set, not stale rows. var snapshotEvents = new List { EventUtils.CreateTestEvent(100) }; - var snapshotData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, snapshotEvents); + var snapshotData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, snapshotEvents); var snapshotState = new EventLogState { @@ -851,7 +852,7 @@ public async Task HandleSetFilters_FilterBranch_WhenLogStillStaleAfterRetry_Shou // mean the pass-2 result is still stale; the slice must be omitted so the reducer's // preserve-omitted fallback keeps the existing rows (avoids losing live events). var snapshotEvents = new List(); - var snapshotData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, snapshotEvents); + var snapshotData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, snapshotEvents); var snapshotState = new EventLogState { @@ -927,7 +928,7 @@ public async Task HandleSetFilters_FilterBranch_WhenLogStillStaleAfterRetry_Shou [Fact] public async Task HandleSetFilters_FilterBranch_WhenSupersededByNewerFilter_ShouldDropStaleResults() { - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); var (effects, mockDispatcher, _, _, mockFilterService) = @@ -995,7 +996,7 @@ public async Task HandleSetFilters_FilterBranch_WhenSupersededByNewerFilter_Shou [Fact] public async Task HandleSetFilters_ReloadBranch_ShouldNotDispatchSetIsLoading() { - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); var (effects, mockDispatcher, _, _, _) = CreateEffectsWithServices(activeLogs: activeLogs); @@ -1019,7 +1020,7 @@ public async Task HandleSetFilters_ShouldFilterAndDispatchUpdate() EventUtils.CreateTestEvent(200, level: Constants.EventLevelInformation) }; - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, events); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, events); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); var (effects, mockDispatcher, _, _, mockFilterService) = CreateEffectsWithServices(activeLogs: activeLogs); @@ -1043,7 +1044,7 @@ public async Task HandleSetFilters_WhenFilterDoesNotRequireXml_ShouldNotReloadLo { // Arrange — single active log + non-XML filter (Id-based). RequiresXml should be false, // so HandleSetFilters should fall through to UpdateDisplayedEvents and never close/open logs. - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); var (effects, mockDispatcher, _, _, _) = CreateEffectsWithServices(activeLogs: activeLogs); @@ -1062,6 +1063,50 @@ public async Task HandleSetFilters_WhenFilterDoesNotRequireXml_ShouldNotReloadLo mockDispatcher.Received(1).Dispatch(Arg.Any()); } + [Fact] + public async Task HandleSetFilters_WhenFilterRequiresXmlAndLogLacksXml_ShouldCloseAndReopenLog() + { + // Arrange — active log has not been loaded with XML, so it must be re-read. + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); + var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); + + var (effects, mockDispatcher, _, _, _) = CreateEffectsWithServices(activeLogs: activeLogs); + + // Route CloseLog → HandleCloseLog so HandleSetFilters' await on the close-completion + // TCS resolves quickly (otherwise it hits LogCloseTimeout, 30s). Capture the routed + // tasks so any fault in HandleCloseLog surfaces at the end of the test instead of + // being swallowed by the discard. + var closeTasks = new List(); + mockDispatcher + .When(d => d.Dispatch(Arg.Any())) + .Do(callInfo => + { + closeTasks.Add(effects.HandleCloseLog(callInfo.Arg(), mockDispatcher)); + }); + + var xmlFilter = FilterUtils.CreateTestFilter(Constants.FilterXmlContainsData, isEnabled: true); + var eventFilter = new EventFilter(null, [xmlFilter]); + var action = new EventLogAction.SetFilters(eventFilter); + + // Act + await effects.HandleSetFilters(action, mockDispatcher); + + // Assert + Assert.True(eventFilter.RequiresXml); + + mockDispatcher.Received(1).Dispatch(Arg.Is(a => + a.LogName == Constants.LogNameTestLog && a.LogId == logData.Id)); + + mockDispatcher.Received(1).Dispatch(Arg.Is(a => + a.LogName == Constants.LogNameTestLog && a.LogPathType == LogPathType.Channel)); + + // Reload path returns early — no UpdateDisplayedEvents until LoadEvents fires. + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + + // Surface any HandleCloseLog faults before exiting the test. + await Task.WhenAll(closeTasks); + } + [Fact] public async Task HandleSetFilters_WhenFilterRequiresXml_AwaitsCloseCompletionBeforeReturning() { @@ -1071,7 +1116,7 @@ public async Task HandleSetFilters_WhenFilterRequiresXml_AwaitsCloseCompletionBe // TCS in its finally block. HandleSetFilters must await that close-completion TCS // before populating _pendingSelectionRestore — otherwise the in-flight close wipes // the freshly-written entry. This test pins down the ordering invariant. - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); var (effects, mockDispatcher, mockLogWatcher, _, _) = CreateEffectsWithServices(activeLogs: activeLogs); @@ -1108,7 +1153,7 @@ public async Task HandleSetFilters_WhenFilterRequiresXml_AwaitsCloseCompletionBe // OpenLog should now have been dispatched (only happens after the close await). mockDispatcher.Received(1).Dispatch(Arg.Is(a => - a.LogName == Constants.LogNameTestLog && a.PathType == PathType.LogName)); + a.LogName == Constants.LogNameTestLog && a.LogPathType == LogPathType.Channel)); // Surface any HandleCloseLog faults before exiting the test. await Task.WhenAll(closeTasks); @@ -1120,7 +1165,7 @@ public async Task HandleSetFilters_WhenFilterRequiresXml_ShouldRestoreSelectionA // Arrange — active log with one previously-selected event (RecordId=42). After the // XML filter triggers a reload, HandleLoadEvents should consume the pending restore // entry and dispatch SelectEvents with the matching event from the new event set. - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); var selectedEvent = EventUtils.CreateTestEvent(100, recordId: 42, logName: Constants.LogNameTestLog); @@ -1196,50 +1241,6 @@ public async Task HandleSetFilters_WhenFilterRequiresXml_ShouldRestoreSelectionA await Task.WhenAll(closeTasks); } - [Fact] - public async Task HandleSetFilters_WhenFilterRequiresXmlAndLogLacksXml_ShouldCloseAndReopenLog() - { - // Arrange — active log has not been loaded with XML, so it must be re-read. - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); - var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); - - var (effects, mockDispatcher, _, _, _) = CreateEffectsWithServices(activeLogs: activeLogs); - - // Route CloseLog → HandleCloseLog so HandleSetFilters' await on the close-completion - // TCS resolves quickly (otherwise it hits LogCloseTimeout, 30s). Capture the routed - // tasks so any fault in HandleCloseLog surfaces at the end of the test instead of - // being swallowed by the discard. - var closeTasks = new List(); - mockDispatcher - .When(d => d.Dispatch(Arg.Any())) - .Do(callInfo => - { - closeTasks.Add(effects.HandleCloseLog(callInfo.Arg(), mockDispatcher)); - }); - - var xmlFilter = FilterUtils.CreateTestFilter(Constants.FilterXmlContainsData, isEnabled: true); - var eventFilter = new EventFilter(null, [xmlFilter]); - var action = new EventLogAction.SetFilters(eventFilter); - - // Act - await effects.HandleSetFilters(action, mockDispatcher); - - // Assert - Assert.True(eventFilter.RequiresXml); - - mockDispatcher.Received(1).Dispatch(Arg.Is(a => - a.LogName == Constants.LogNameTestLog && a.LogId == logData.Id)); - - mockDispatcher.Received(1).Dispatch(Arg.Is(a => - a.LogName == Constants.LogNameTestLog && a.PathType == PathType.LogName)); - - // Reload path returns early — no UpdateDisplayedEvents until LoadEvents fires. - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); - - // Surface any HandleCloseLog faults before exiting the test. - await Task.WhenAll(closeTasks); - } - [Fact] public async Task PrepareForDatabaseRemovalAsync_DispatchesCloseLogPerActiveLog_PopulatesSnapshotInCloseOrder() { @@ -1247,8 +1248,8 @@ public async Task PrepareForDatabaseRemovalAsync_DispatchesCloseLogPerActiveLog_ // HandleCloseLog so the close-completion TCSes get signaled. This proves the sink // pattern (snapshot is populated incrementally) rather than relying on the live // Fluxor pipeline. - var log1 = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); - var log2 = new EventLogData(Constants.LogNameLog2, PathType.LogName, []); + var log1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); + var log2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); var log1Id = log1.Id; var log2Id = log2.Id; @@ -1276,8 +1277,8 @@ public async Task PrepareForDatabaseRemovalAsync_DispatchesCloseLogPerActiveLog_ // Assert — snapshot has both logs (order matches the ActiveLogs iteration). Assert.Equal(2, snapshot.Items.Count); - Assert.Contains(snapshot.Items, item => item.Name == Constants.LogNameLog1 && item.Type == PathType.LogName); - Assert.Contains(snapshot.Items, item => item.Name == Constants.LogNameLog2 && item.Type == PathType.LogName); + Assert.Contains(snapshot.Items, item => item.Name == Constants.LogNameLog1 && item.Type == LogPathType.Channel); + Assert.Contains(snapshot.Items, item => item.Name == Constants.LogNameLog2 && item.Type == LogPathType.Channel); // Both CloseLog actions should have been dispatched. mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.LogId == log1Id)); @@ -1290,7 +1291,7 @@ public async Task PrepareForDatabaseRemovalAsync_WhenCancellationTokenCancelsBef // Arrange — one active log, dispatcher does NOT route back. The close-completion // TCS will never be signaled, so the await with a cancelled token should throw and // the sink should remain empty. - var log = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); + var log = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameLog1, log); var (effects, _, _, _, _) = CreateEffectsWithServices(activeLogs: activeLogs); @@ -1332,8 +1333,8 @@ public void ReopenAfterDatabaseRemoval_DispatchesOpenLogPerSnapshotEntry() var snapshot = new[] { - new LogReopenInfo(Constants.LogNameLog1, PathType.LogName), - new LogReopenInfo(Constants.LogNameLog2, PathType.FilePath) + new LogReopenInfo(Constants.LogNameLog1, LogPathType.Channel), + new LogReopenInfo(Constants.LogNameLog2, LogPathType.File) }; // Act @@ -1341,9 +1342,9 @@ public void ReopenAfterDatabaseRemoval_DispatchesOpenLogPerSnapshotEntry() // Assert mockDispatcher.Received(1).Dispatch(Arg.Is(a => - a.LogName == Constants.LogNameLog1 && a.PathType == PathType.LogName)); + a.LogName == Constants.LogNameLog1 && a.LogPathType == LogPathType.Channel)); mockDispatcher.Received(1).Dispatch(Arg.Is(a => - a.LogName == Constants.LogNameLog2 && a.PathType == PathType.FilePath)); + a.LogName == Constants.LogNameLog2 && a.LogPathType == LogPathType.File)); } private static (EventLogEffects effects, IDispatcher mockDispatcher) CreateEffects( @@ -1419,22 +1420,18 @@ private static (EventLogEffects effects, IDispatcher mockDispatcher) CreateEffec private static (EventLogEffects effects, IDispatcher mockDispatcher, - IFilterService mockFilterService) CreateEffectsWithMutableState(Func stateProvider) + IServiceProvider mockServiceProvider, + IBannerService mockBannerService, + IDatabaseService mockDatabaseService) CreateEffectsForOpenLogGuards( + ImmutableDictionary activeLogs) { var mockEventLogState = Substitute.For>(); - mockEventLogState.Value.Returns(_ => stateProvider()); - - var mockFilterService = Substitute.For(); - - mockFilterService.FilterActiveLogs(Arg.Any>(), Arg.Any()) - .Returns(new Dictionary>()); - - mockFilterService.GetFilteredEvents(Arg.Any>(), Arg.Any()) - .Returns(callInfo => callInfo.Arg>().ToList()); - var mockLogger = Substitute.For(); - var mockLogWatcherService = Substitute.For(); - var mockResolverCache = Substitute.For(); + mockEventLogState.Value.Returns(new EventLogState + { + ActiveLogs = activeLogs, + AppliedFilter = new EventFilter(null, []) + }); var mockServiceScopeFactory = Substitute.For(); var mockServiceScope = Substitute.For(); @@ -1443,40 +1440,53 @@ private static (EventLogEffects effects, mockServiceScopeFactory.CreateScope().Returns(mockServiceScope); mockServiceScope.ServiceProvider.Returns(mockServiceProvider); + var mockEventResolver = Substitute.For(); + + mockEventResolver.ResolveEvent(Arg.Any()) + .Returns(_ => EventUtils.CreateTestEvent(100)); + + mockServiceProvider.GetService(typeof(IEventResolver)).Returns(mockEventResolver); + var mockDatabaseService = Substitute.For(); mockDatabaseService.InitialClassificationTask.Returns(Task.CompletedTask); + var mockBannerService = Substitute.For(); + var mockDispatcher = Substitute.For(); var effects = new EventLogEffects( mockEventLogState, - mockFilterService, - mockLogger, - mockLogWatcherService, - mockResolverCache, + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), Substitute.For(), mockServiceScopeFactory, mockDatabaseService, - Substitute.For(), + mockBannerService, mockDispatcher); - return (effects, mockDispatcher, mockFilterService); + return (effects, mockDispatcher, mockServiceProvider, mockBannerService, mockDatabaseService); } private static (EventLogEffects effects, IDispatcher mockDispatcher, - IServiceProvider mockServiceProvider, - IBannerService mockBannerService, - IDatabaseService mockDatabaseService) CreateEffectsForOpenLogGuards( - ImmutableDictionary activeLogs) + IFilterService mockFilterService) CreateEffectsWithMutableState(Func stateProvider) { var mockEventLogState = Substitute.For>(); + mockEventLogState.Value.Returns(_ => stateProvider()); - mockEventLogState.Value.Returns(new EventLogState - { - ActiveLogs = activeLogs, - AppliedFilter = new EventFilter(null, []) - }); + var mockFilterService = Substitute.For(); + + mockFilterService.FilterActiveLogs(Arg.Any>(), Arg.Any()) + .Returns(new Dictionary>()); + + mockFilterService.GetFilteredEvents(Arg.Any>(), Arg.Any()) + .Returns(callInfo => callInfo.Arg>().ToList()); + + var mockLogger = Substitute.For(); + var mockLogWatcherService = Substitute.For(); + var mockResolverCache = Substitute.For(); var mockServiceScopeFactory = Substitute.For(); var mockServiceScope = Substitute.For(); @@ -1485,33 +1495,24 @@ private static (EventLogEffects effects, mockServiceScopeFactory.CreateScope().Returns(mockServiceScope); mockServiceScope.ServiceProvider.Returns(mockServiceProvider); - var mockEventResolver = Substitute.For(); - - mockEventResolver.ResolveEvent(Arg.Any()) - .Returns(_ => EventUtils.CreateTestEvent(100)); - - mockServiceProvider.GetService(typeof(IEventResolver)).Returns(mockEventResolver); - var mockDatabaseService = Substitute.For(); mockDatabaseService.InitialClassificationTask.Returns(Task.CompletedTask); - var mockBannerService = Substitute.For(); - var mockDispatcher = Substitute.For(); var effects = new EventLogEffects( mockEventLogState, - Substitute.For(), - Substitute.For(), - Substitute.For(), - Substitute.For(), + mockFilterService, + mockLogger, + mockLogWatcherService, + mockResolverCache, Substitute.For(), mockServiceScopeFactory, mockDatabaseService, - mockBannerService, + Substitute.For(), mockDispatcher); - return (effects, mockDispatcher, mockServiceProvider, mockBannerService, mockDatabaseService); + return (effects, mockDispatcher, mockFilterService); } private static (EventLogEffects effects, diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.cs index 259e505c..be27a754 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; -using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Common.Channels; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Tests.TestUtils; @@ -39,19 +39,6 @@ public void BufferedEventsWorkflow_ShouldHandleCorrectly() Assert.True(state.ContinuouslyUpdate); } - [Fact] - public void EventLogAction_AddEvent_ShouldStoreNewEvent() - { - // Arrange - var newEvent = EventUtils.CreateTestEvent(100); - - // Act - var action = new EventLogAction.AddEvent(newEvent); - - // Assert - Assert.Equal(newEvent, action.NewEvent); - } - [Fact] public void EventLogAction_AddEventBuffered_ShouldStoreBufferAndFullFlag() { @@ -74,7 +61,7 @@ public void EventLogAction_AddEventBuffered_ShouldStoreBufferAndFullFlag() public void EventLogAction_AddEventSuccess_ShouldStoreActiveLogs() { // Arrange - var logData = new EventLogData("TestLog", PathType.LogName, []); + var logData = new EventLogData("TestLog", LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); // Act @@ -85,6 +72,19 @@ public void EventLogAction_AddEventSuccess_ShouldStoreActiveLogs() Assert.True(action.ActiveLogs.ContainsKey(Constants.LogNameTestLog)); } + [Fact] + public void EventLogAction_AddEvent_ShouldStoreNewEvent() + { + // Arrange + var newEvent = EventUtils.CreateTestEvent(100); + + // Act + var action = new EventLogAction.AddEvent(newEvent); + + // Assert + Assert.Equal(newEvent, action.NewEvent); + } + [Fact] public void EventLogAction_CloseLog_ShouldStoreLogIdAndName() { @@ -104,7 +104,7 @@ public void EventLogAction_CloseLog_ShouldStoreLogIdAndName() public void EventLogAction_LoadEvents_ShouldStoreLogDataAndEvents() { // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var events = ImmutableArray.Create(EventUtils.CreateTestEvent(100), EventUtils.CreateTestEvent(200)); @@ -121,14 +121,14 @@ public void EventLogAction_OpenLog_ShouldStoreLogNameAndPathType() { // Arrange var logName = Constants.LogNameApplication; - var pathType = PathType.LogName; + var pathType = LogPathType.Channel; // Act var action = new EventLogAction.OpenLog(logName, pathType); // Assert Assert.Equal(logName, action.LogName); - Assert.Equal(pathType, action.PathType); + Assert.Equal(pathType, action.LogPathType); } [Fact] @@ -138,7 +138,7 @@ public void EventLogAction_OpenLog_WithCancellationToken_ShouldStoreToken() var cts = new CancellationTokenSource(); // Act - var action = new EventLogAction.OpenLog(Constants.LogNameTestLog, PathType.FilePath, cts.Token); + var action = new EventLogAction.OpenLog(Constants.LogNameTestLog, LogPathType.File, cts.Token); // Assert Assert.Equal(cts.Token, action.Token); @@ -220,7 +220,7 @@ public void EventLogData_Constructor_ShouldSetProperties() { // Arrange var name = Constants.LogNameTestLog; - var type = PathType.LogName; + var type = LogPathType.Channel; var events = new List { EventUtils.CreateTestEvent(100) }; // Act @@ -243,7 +243,7 @@ public void EventLogData_GetCategoryValues_ForId_ShouldReturnDistinctIds() EventUtils.CreateTestEvent(200) }; - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, events); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, events); // Act var values = logData.GetCategoryValues(FilterCategory.Id).ToList(); @@ -258,7 +258,7 @@ public void EventLogData_GetCategoryValues_ForId_ShouldReturnDistinctIds() public void EventLogData_GetCategoryValues_ForLevel_ShouldReturnAllSeverityLevels() { // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); // Act var values = logData.GetCategoryValues(FilterCategory.Level).ToList(); @@ -278,7 +278,7 @@ public void EventLogData_GetCategoryValues_ForSource_ShouldReturnDistinctSources EventUtils.CreateTestEvent(300, Constants.EventSourceOtherSource) }; - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, events); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, events); // Act var values = logData.GetCategoryValues(FilterCategory.Source).ToList(); @@ -291,7 +291,7 @@ public void EventLogData_GetCategoryValues_ForSource_ShouldReturnDistinctSources public void EventLogData_GetCategoryValues_ForUnknownCategory_ShouldReturnEmpty() { // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, [EventUtils.CreateTestEvent(100)]); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, [EventUtils.CreateTestEvent(100)]); // Act var values = logData.GetCategoryValues((FilterCategory)999).ToList(); @@ -304,8 +304,8 @@ public void EventLogData_GetCategoryValues_ForUnknownCategory_ShouldReturnEmpty( public void EventLogData_Id_ShouldBeUnique() { // Arrange & Act - var logData1 = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); - var logData2 = new EventLogData(Constants.LogNameLog2, PathType.LogName, []); + var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); + var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); // Assert Assert.NotEqual(logData1.Id, logData2.Id); @@ -344,7 +344,7 @@ public void LoadEventsAndSelect_ShouldWorkCorrectly() var state = new EventLogState(); state = EventLogReducers.ReduceOpenLog(state, - new EventLogAction.OpenLog(Constants.LogNameTestLog, PathType.LogName)); + new EventLogAction.OpenLog(Constants.LogNameTestLog, LogPathType.Channel)); var logData = state.ActiveLogs[Constants.LogNameTestLog]; @@ -376,13 +376,13 @@ public void MultipleLogOperations_ShouldMaintainCorrectState() // Act - Open multiple logs state = EventLogReducers.ReduceOpenLog(state, - new EventLogAction.OpenLog(Constants.LogNameLog1, PathType.LogName)); + new EventLogAction.OpenLog(Constants.LogNameLog1, LogPathType.Channel)); state = EventLogReducers.ReduceOpenLog(state, - new EventLogAction.OpenLog(Constants.LogNameLog2, PathType.FilePath)); + new EventLogAction.OpenLog(Constants.LogNameLog2, LogPathType.File)); state = EventLogReducers.ReduceOpenLog(state, - new EventLogAction.OpenLog(Constants.LogNameLog3, PathType.LogName)); + new EventLogAction.OpenLog(Constants.LogNameLog3, LogPathType.Channel)); // Assert Assert.Equal(3, state.ActiveLogs.Count); @@ -405,7 +405,7 @@ public void OpenAndCloseLog_ShouldMaintainStateConsistency() var state = new EventLogState(); // Act - Open log - var openAction = new EventLogAction.OpenLog(Constants.LogNameTestLog, PathType.LogName); + var openAction = new EventLogAction.OpenLog(Constants.LogNameTestLog, LogPathType.Channel); state = EventLogReducers.ReduceOpenLog(state, openAction); Assert.Single(state.ActiveLogs); @@ -461,7 +461,7 @@ public void ReduceAddEventSuccess_ShouldUpdateActiveLogs() { // Arrange var state = new EventLogState(); - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); var action = new EventLogAction.AddEventSuccess(activeLogs); @@ -477,7 +477,7 @@ public void ReduceAddEventSuccess_ShouldUpdateActiveLogs() public void ReduceCloseAll_ShouldClearAllState() { // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventLogState { @@ -515,21 +515,21 @@ public void ReduceCloseAll_ShouldClearSelectedEvent() public void ReduceCloseLog_ShouldFilterNewEventBuffer() { // Arrange - var eventForLog1 = new DisplayEventModel(Constants.LogNameLog1, PathType.LogName) + var eventForLog1 = new DisplayEventModel(Constants.LogNameLog1, LogPathType.Channel) { Id = 100, Source = Constants.EventSourceTestSource, Level = Constants.EventLevelInformation }; - var eventForLog2 = new DisplayEventModel(Constants.LogNameLog2, PathType.LogName) + var eventForLog2 = new DisplayEventModel(Constants.LogNameLog2, LogPathType.Channel) { Id = 200, Source = Constants.EventSourceTestSource, Level = Constants.EventLevelInformation }; - var logData1 = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); + var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); var state = new EventLogState { @@ -551,8 +551,8 @@ public void ReduceCloseLog_ShouldFilterNewEventBuffer() public void ReduceCloseLog_ShouldRemoveSpecifiedLog() { // Arrange - var logData1 = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); - var logData2 = new EventLogData(Constants.LogNameLog2, PathType.LogName, []); + var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); + var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); var state = new EventLogState { @@ -572,11 +572,50 @@ public void ReduceCloseLog_ShouldRemoveSpecifiedLog() Assert.True(newState.ActiveLogs.ContainsKey(Constants.LogNameLog2)); } + [Fact] + public void ReduceLoadEventsPartial_WhenLogIdDoesNotMatch_ShouldReturnStateUnchanged() + { + // Arrange + var state = new EventLogState(); + + state = EventLogReducers.ReduceOpenLog(state, + new EventLogAction.OpenLog(Constants.LogNameTestLog, LogPathType.Channel)); + + var staleLogData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); + var events = ImmutableArray.Create(EventUtils.CreateTestEvent(100)); + + // Act — stale partial with mismatched ID + var newState = EventLogReducers.ReduceLoadEventsPartial(state, + new EventLogAction.LoadEventsPartial(staleLogData, events)); + + // Assert — state unchanged, original log preserved with its ID + var originalId = state.ActiveLogs[Constants.LogNameTestLog].Id; + Assert.NotEqual(originalId, staleLogData.Id); + Assert.Equal(originalId, newState.ActiveLogs[Constants.LogNameTestLog].Id); + Assert.Empty(newState.ActiveLogs[Constants.LogNameTestLog].Events); + } + + [Fact] + public void ReduceLoadEventsPartial_WhenLogNotInActiveLogs_ShouldReturnStateUnchanged() + { + // Arrange — no logs open + var state = new EventLogState(); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); + var events = ImmutableArray.Create(EventUtils.CreateTestEvent(100)); + + // Act + var newState = EventLogReducers.ReduceLoadEventsPartial(state, + new EventLogAction.LoadEventsPartial(logData, events)); + + // Assert + Assert.Same(state, newState); + } + [Fact] public void ReduceLoadEvents_ShouldIsolateStateFromOriginalList() { // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventLogState { @@ -602,7 +641,7 @@ public void ReduceLoadEvents_ShouldIsolateStateFromOriginalList() public void ReduceLoadEvents_ShouldUpdateLogWithEvents() { // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventLogState { @@ -627,10 +666,10 @@ public void ReduceLoadEvents_WhenLogIdDoesNotMatch_ShouldReturnStateUnchanged() var state = new EventLogState(); state = EventLogReducers.ReduceOpenLog(state, - new EventLogAction.OpenLog(Constants.LogNameTestLog, PathType.LogName)); + new EventLogAction.OpenLog(Constants.LogNameTestLog, LogPathType.Channel)); // Create stale logData with a new ID (simulating a previous load instance) - var staleLogData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var staleLogData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var events = ImmutableArray.Create(EventUtils.CreateTestEvent(100)); // Act — stale LoadEvents with mismatched ID @@ -650,7 +689,7 @@ public void ReduceLoadEvents_WhenLogIdMatches_ShouldUpdateLog() var state = new EventLogState(); state = EventLogReducers.ReduceOpenLog(state, - new EventLogAction.OpenLog(Constants.LogNameTestLog, PathType.LogName)); + new EventLogAction.OpenLog(Constants.LogNameTestLog, LogPathType.Channel)); var logData = state.ActiveLogs[Constants.LogNameTestLog]; var events = ImmutableArray.Create(EventUtils.CreateTestEvent(100)); @@ -667,7 +706,7 @@ public void ReduceLoadEvents_WhenLogNotInActiveLogs_ShouldReturnStateUnchanged() { // Arrange — no logs open var state = new EventLogState(); - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var events = ImmutableArray.Create(EventUtils.CreateTestEvent(100)); // Act — stale LoadEvents arrives for a closed log @@ -678,51 +717,12 @@ public void ReduceLoadEvents_WhenLogNotInActiveLogs_ShouldReturnStateUnchanged() Assert.Empty(newState.ActiveLogs); } - [Fact] - public void ReduceLoadEventsPartial_WhenLogIdDoesNotMatch_ShouldReturnStateUnchanged() - { - // Arrange - var state = new EventLogState(); - - state = EventLogReducers.ReduceOpenLog(state, - new EventLogAction.OpenLog(Constants.LogNameTestLog, PathType.LogName)); - - var staleLogData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); - var events = ImmutableArray.Create(EventUtils.CreateTestEvent(100)); - - // Act — stale partial with mismatched ID - var newState = EventLogReducers.ReduceLoadEventsPartial(state, - new EventLogAction.LoadEventsPartial(staleLogData, events)); - - // Assert — state unchanged, original log preserved with its ID - var originalId = state.ActiveLogs[Constants.LogNameTestLog].Id; - Assert.NotEqual(originalId, staleLogData.Id); - Assert.Equal(originalId, newState.ActiveLogs[Constants.LogNameTestLog].Id); - Assert.Empty(newState.ActiveLogs[Constants.LogNameTestLog].Events); - } - - [Fact] - public void ReduceLoadEventsPartial_WhenLogNotInActiveLogs_ShouldReturnStateUnchanged() - { - // Arrange — no logs open - var state = new EventLogState(); - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); - var events = ImmutableArray.Create(EventUtils.CreateTestEvent(100)); - - // Act - var newState = EventLogReducers.ReduceLoadEventsPartial(state, - new EventLogAction.LoadEventsPartial(logData, events)); - - // Assert - Assert.Same(state, newState); - } - [Fact] public void ReduceOpenLog_ShouldAddEmptyLogData() { // Arrange var state = new EventLogState(); - var action = new EventLogAction.OpenLog(Constants.LogNameNewLog, PathType.LogName); + var action = new EventLogAction.OpenLog(Constants.LogNameNewLog, LogPathType.Channel); // Act var newState = EventLogReducers.ReduceOpenLog(state, action); @@ -741,13 +741,13 @@ public void ReduceOpenLog_WhenLogAlreadyActive_ShouldReturnSameStateInstance() // (which would invalidate ongoing loads and reset the user's events). var state = new EventLogState(); state = EventLogReducers.ReduceOpenLog(state, - new EventLogAction.OpenLog(Constants.LogNameTestLog, PathType.LogName)); + new EventLogAction.OpenLog(Constants.LogNameTestLog, LogPathType.Channel)); var existingLogData = state.ActiveLogs[Constants.LogNameTestLog]; // Act — dispatch the same OpenLog a second time. var newState = EventLogReducers.ReduceOpenLog(state, - new EventLogAction.OpenLog(Constants.LogNameTestLog, PathType.LogName)); + new EventLogAction.OpenLog(Constants.LogNameTestLog, LogPathType.Channel)); // Assert — same state reference, same EventLogData reference (no replacement). Assert.Same(state, newState); @@ -760,13 +760,13 @@ public void ReduceOpenLog_WithFilePath_ShouldSetCorrectPathType() { // Arrange var state = new EventLogState(); - var action = new EventLogAction.OpenLog(Constants.FilePathTestEvtx, PathType.FilePath); + var action = new EventLogAction.OpenLog(Constants.FilePathTestEvtx, LogPathType.File); // Act var newState = EventLogReducers.ReduceOpenLog(state, action); // Assert - Assert.Equal(PathType.FilePath, newState.ActiveLogs[Constants.FilePathTestEvtx].Type); + Assert.Equal(LogPathType.File, newState.ActiveLogs[Constants.FilePathTestEvtx].Type); } [Fact] @@ -820,36 +820,36 @@ public void ReduceSelectEvent_WhenEventNotSelected_ShouldAddEvent() } [Fact] - public void ReduceSelectEvent_WhenMultiSelect_ShouldAddToExisting() + public void ReduceSelectEvent_WhenMultiSelectAndEventAlreadySelected_ShouldRemoveEvent() { // Arrange - var existingEvent = EventUtils.CreateTestEvent(100); - var state = new EventLogState { SelectedEvents = [existingEvent] }; - var newEvent = EventUtils.CreateTestEvent(200); - var action = new EventLogAction.SelectEvent(newEvent, true); + var selectedEvent = EventUtils.CreateTestEvent(100); + var state = new EventLogState { SelectedEvents = [selectedEvent] }; + var action = new EventLogAction.SelectEvent(selectedEvent, true); // Act var newState = EventLogReducers.ReduceSelectEvent(state, action); // Assert - Assert.Equal(2, newState.SelectedEvents.Count); - Assert.Contains(existingEvent, newState.SelectedEvents); - Assert.Contains(newEvent, newState.SelectedEvents); + Assert.Empty(newState.SelectedEvents); } [Fact] - public void ReduceSelectEvent_WhenMultiSelectAndEventAlreadySelected_ShouldRemoveEvent() + public void ReduceSelectEvent_WhenMultiSelect_ShouldAddToExisting() { // Arrange - var selectedEvent = EventUtils.CreateTestEvent(100); - var state = new EventLogState { SelectedEvents = [selectedEvent] }; - var action = new EventLogAction.SelectEvent(selectedEvent, true); + var existingEvent = EventUtils.CreateTestEvent(100); + var state = new EventLogState { SelectedEvents = [existingEvent] }; + var newEvent = EventUtils.CreateTestEvent(200); + var action = new EventLogAction.SelectEvent(newEvent, true); // Act var newState = EventLogReducers.ReduceSelectEvent(state, action); // Assert - Assert.Empty(newState.SelectedEvents); + Assert.Equal(2, newState.SelectedEvents.Count); + Assert.Contains(existingEvent, newState.SelectedEvents); + Assert.Contains(newEvent, newState.SelectedEvents); } [Fact] diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EventTableStoreTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EventTableStoreTests.cs index 949f71d8..94b2e2ea 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EventTableStoreTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EventTableStoreTests.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; -using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Common.Channels; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.EventTable; using EventLogExpert.UI.Tests.TestUtils; @@ -17,7 +17,7 @@ public sealed class EventTableStoreTests public void EventTableAction_AddTable_ShouldStoreLogData() { // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); // Act var action = new EventTableAction.AddTable(logData); @@ -166,13 +166,13 @@ public void EventTableAction_UpdateTable_ShouldStoreLogIdAndEvents() public void EventTableModel_ComputerName_AfterFirstEventArrives_ShouldBeStoredOnTable() { // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); var firstBatch = new List { - new(Constants.LogNameTestLog, PathType.LogName) { Id = 10, RecordId = 1, ComputerName = Constants.EventComputerServer01 } + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 10, RecordId = 1, ComputerName = Constants.EventComputerServer01 } }; state = EventTableReducers.ReduceAppendTableEvents( @@ -181,7 +181,7 @@ public void EventTableModel_ComputerName_AfterFirstEventArrives_ShouldBeStoredOn var secondBatch = new List { - new(Constants.LogNameTestLog, PathType.LogName) { Id = 11, RecordId = 2, ComputerName = Constants.EventComputerServer02 } + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 11, RecordId = 2, ComputerName = Constants.EventComputerServer02 } }; // Act — second batch with a different ComputerName must not overwrite the latched value @@ -198,14 +198,14 @@ public void EventTableModel_ComputerName_AfterFirstEventArrives_ShouldBeStoredOn public void EventTableModel_ComputerName_WhenFirstEventHasEmptyComputerName_ShouldUseLaterEventInBatch() { // Arrange — first event has an empty ComputerName (resolver miss); second carries the name - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); var batch = new List { - new(Constants.LogNameTestLog, PathType.LogName) { Id = 10, RecordId = 1, ComputerName = string.Empty }, - new(Constants.LogNameTestLog, PathType.LogName) { Id = 11, RecordId = 2, ComputerName = Constants.EventComputerServer01 } + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 10, RecordId = 1, ComputerName = string.Empty }, + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 11, RecordId = 2, ComputerName = Constants.EventComputerServer01 } }; // Act @@ -287,7 +287,7 @@ public void IntegrationTest_LoadAndUpdateTableEvents() { // Arrange var state = new EventTableState(); - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); // Act - Add table state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); @@ -312,9 +312,9 @@ public void IntegrationTest_OpenMultipleLogsAndCloseOne() { // Arrange var state = new EventTableState(); - var logData1 = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); - var logData2 = new EventLogData(Constants.LogNameLog2, PathType.LogName, []); - var logData3 = new EventLogData(Constants.LogNameLog3, PathType.LogName, []); + var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); + var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); + var logData3 = new EventLogData(Constants.LogNameLog3, LogPathType.Channel, []); // Act - Open three logs state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); @@ -338,13 +338,13 @@ public void IntegrationTest_OpenMultipleLogsAndCloseOne() public void ReduceAddTable_WhenCombinedExists_ShouldNotCreateAnotherCombined() { // Arrange - var logData1 = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); - var logData2 = new EventLogData(Constants.LogNameLog2, PathType.LogName, []); + var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); + var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); - var logData3 = new EventLogData(Constants.LogNameLog3, PathType.LogName, []); + var logData3 = new EventLogData(Constants.LogNameLog3, LogPathType.Channel, []); var action = new EventTableAction.AddTable(logData3); // Act @@ -360,7 +360,7 @@ public void ReduceAddTable_WhenFirstTable_ShouldBeLoading() { // Arrange var state = new EventTableState(); - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var action = new EventTableAction.AddTable(logData); // Act @@ -375,7 +375,7 @@ public void ReduceAddTable_WhenFirstTable_ShouldSetAsActive() { // Arrange var state = new EventTableState(); - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var action = new EventTableAction.AddTable(logData); // Act @@ -393,7 +393,7 @@ public void ReduceAddTable_WhenFirstTable_WithFilePath_ShouldSetFileName() { // Arrange var state = new EventTableState(); - var logData = new EventLogData(Constants.FilePathTestEvtx, PathType.FilePath, []); + var logData = new EventLogData(Constants.FilePathTestEvtx, LogPathType.File, []); var action = new EventTableAction.AddTable(logData); // Act @@ -401,7 +401,7 @@ public void ReduceAddTable_WhenFirstTable_WithFilePath_ShouldSetFileName() // Assert Assert.Equal(Constants.FilePathTestEvtx, newState.EventTables.First().FileName); - Assert.Equal(PathType.FilePath, newState.EventTables.First().PathType); + Assert.Equal(LogPathType.File, newState.EventTables.First().LogPathType); } [Fact] @@ -409,7 +409,7 @@ public void ReduceAddTable_WhenFirstTable_WithLogName_ShouldNotSetFileName() { // Arrange var state = new EventTableState(); - var logData = new EventLogData(Constants.LogNameApplication, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameApplication, LogPathType.Channel, []); var action = new EventTableAction.AddTable(logData); // Act @@ -418,18 +418,18 @@ public void ReduceAddTable_WhenFirstTable_WithLogName_ShouldNotSetFileName() // Assert Assert.Null(newState.EventTables.First().FileName); Assert.Equal(Constants.LogNameApplication, newState.EventTables.First().LogName); - Assert.Equal(PathType.LogName, newState.EventTables.First().PathType); + Assert.Equal(LogPathType.Channel, newState.EventTables.First().LogPathType); } [Fact] public void ReduceAddTable_WhenSecondTable_ShouldCreateCombinedTable() { // Arrange - var logData1 = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); + var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); - var logData2 = new EventLogData(Constants.LogNameLog2, PathType.LogName, []); + var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); var action = new EventTableAction.AddTable(logData2); // Act @@ -444,11 +444,11 @@ public void ReduceAddTable_WhenSecondTable_ShouldCreateCombinedTable() public void ReduceAddTable_WhenSecondTable_ShouldSetCombinedAsActive() { // Arrange - var logData1 = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); + var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); - var logData2 = new EventLogData(Constants.LogNameLog2, PathType.LogName, []); + var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); var action = new EventTableAction.AddTable(logData2); // Act @@ -459,11 +459,38 @@ public void ReduceAddTable_WhenSecondTable_ShouldSetCombinedAsActive() Assert.Equal(combinedTable.Id, newState.ActiveEventLogId); } + [Fact] + public void ReduceAppendTableEventsBatch_WhenBatchTargetsClosedLog_ShouldSkipThatBatch() + { + // Arrange — open log plus a stale log id whose tab no longer exists (race: closed mid-flight) + var openLog = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); + var state = new EventTableState(); + state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(openLog)); + + var staleLogId = EventLogId.Create(); + var batches = new Dictionary> + { + { openLog.Id, [new(Constants.LogNameLog1, LogPathType.Channel) { Id = 10, RecordId = 1 }] }, + { staleLogId, [new("ClosedLog", LogPathType.Channel) { Id = 99, RecordId = 99 }] } + }; + + // Act + var newState = EventTableReducers.ReduceAppendTableEventsBatch( + state, + new EventTableAction.AppendTableEventsBatch(batches)); + + // Assert — stale batch is skipped; canonical and EventCountByLog only reflect the open log + Assert.Single(newState.DisplayedEvents); + Assert.Equal(1L, newState.DisplayedEvents[0].RecordId); + Assert.False(newState.EventCountByLog.ContainsKey(staleLogId)); + Assert.Equal(1, newState.EventCountByLog[openLog.Id]); + } + [Fact] public void ReduceAppendTableEvents_ShouldAppendEventsToExistingDisplayedEvents() { // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); @@ -498,7 +525,7 @@ public void ReduceAppendTableEvents_ShouldAppendEventsToExistingDisplayedEvents( public void ReduceAppendTableEvents_ShouldNotChangeIsLoading() { // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); @@ -522,7 +549,7 @@ public void ReduceAppendTableEvents_ShouldNotChangeIsLoading() public void ReduceAppendTableEvents_ShouldPreserveSortOrder() { // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); @@ -561,7 +588,7 @@ public void ReduceAppendTableEvents_ShouldPreserveSortOrder() public void ReduceAppendTableEvents_WhenTableNotFound_ShouldReturnUnchangedState() { // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); @@ -578,38 +605,11 @@ public void ReduceAppendTableEvents_WhenTableNotFound_ShouldReturnUnchangedState Assert.Same(state, newState); } - [Fact] - public void ReduceAppendTableEventsBatch_WhenBatchTargetsClosedLog_ShouldSkipThatBatch() - { - // Arrange — open log plus a stale log id whose tab no longer exists (race: closed mid-flight) - var openLog = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); - var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(openLog)); - - var staleLogId = EventLogId.Create(); - var batches = new Dictionary> - { - { openLog.Id, [new(Constants.LogNameLog1, PathType.LogName) { Id = 10, RecordId = 1 }] }, - { staleLogId, [new("ClosedLog", PathType.LogName) { Id = 99, RecordId = 99 }] } - }; - - // Act - var newState = EventTableReducers.ReduceAppendTableEventsBatch( - state, - new EventTableAction.AppendTableEventsBatch(batches)); - - // Assert — stale batch is skipped; canonical and EventCountByLog only reflect the open log - Assert.Single(newState.DisplayedEvents); - Assert.Equal(1L, newState.DisplayedEvents[0].RecordId); - Assert.False(newState.EventCountByLog.ContainsKey(staleLogId)); - Assert.Equal(1, newState.EventCountByLog[openLog.Id]); - } - [Fact] public void ReduceCloseAll_ShouldClearAllTables() { // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); @@ -626,9 +626,9 @@ public void ReduceCloseLog_ShouldScrubCanonicalRowsAndCountForClosedLog() { // Arrange — three logs with canonical rows; close the middle one. Canonical must lose // only the closed log's rows, and EventCountByLog must drop the closed log's id. - var logData1 = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); - var logData2 = new EventLogData(Constants.LogNameLog2, PathType.LogName, []); - var logData3 = new EventLogData(Constants.LogNameLog3, PathType.LogName, []); + var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); + var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); + var logData3 = new EventLogData(Constants.LogNameLog3, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); @@ -636,18 +636,18 @@ public void ReduceCloseLog_ShouldScrubCanonicalRowsAndCountForClosedLog() var log1Events = new List { - new(Constants.LogNameLog1, PathType.LogName) { Id = 100, RecordId = 1 } + new(Constants.LogNameLog1, LogPathType.Channel) { Id = 100, RecordId = 1 } }; var log2Events = new List { - new(Constants.LogNameLog2, PathType.LogName) { Id = 200, RecordId = 1 }, - new(Constants.LogNameLog2, PathType.LogName) { Id = 201, RecordId = 2 } + new(Constants.LogNameLog2, LogPathType.Channel) { Id = 200, RecordId = 1 }, + new(Constants.LogNameLog2, LogPathType.Channel) { Id = 201, RecordId = 2 } }; var log3Events = new List { - new(Constants.LogNameLog3, PathType.LogName) { Id = 300, RecordId = 1 } + new(Constants.LogNameLog3, LogPathType.Channel) { Id = 300, RecordId = 1 } }; state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData1.Id, log1Events)); @@ -671,21 +671,21 @@ public void ReduceCloseLog_WhenClosingDownToOneLog_ShouldKeepOnlySoleRemainingLo // Arrange — two logs with canonical rows; close one. The Combined table is removed and // canonical is filtered down to just the remaining log's rows. Exercises the case-1 // branch (which uses FilterByOwningLog rather than FilterOutOwningLog). - var logData1 = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); - var logData2 = new EventLogData(Constants.LogNameLog2, PathType.LogName, []); + var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); + var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); var log1Events = new List { - new(Constants.LogNameLog1, PathType.LogName) { Id = 100, RecordId = 1 } + new(Constants.LogNameLog1, LogPathType.Channel) { Id = 100, RecordId = 1 } }; var log2Events = new List { - new(Constants.LogNameLog2, PathType.LogName) { Id = 200, RecordId = 1 }, - new(Constants.LogNameLog2, PathType.LogName) { Id = 201, RecordId = 2 } + new(Constants.LogNameLog2, LogPathType.Channel) { Id = 200, RecordId = 1 }, + new(Constants.LogNameLog2, LogPathType.Channel) { Id = 201, RecordId = 2 } }; state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData1.Id, log1Events)); @@ -705,7 +705,7 @@ public void ReduceCloseLog_WhenClosingDownToOneLog_ShouldKeepOnlySoleRemainingLo public void ReduceCloseLog_WhenLastTable_ShouldClearAll() { // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); @@ -723,9 +723,9 @@ public void ReduceCloseLog_WhenLastTable_ShouldClearAll() public void ReduceCloseLog_WhenThreeTables_ShouldKeepCombinedAndRemainingTables() { // Arrange - var logData1 = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); - var logData2 = new EventLogData(Constants.LogNameLog2, PathType.LogName, []); - var logData3 = new EventLogData(Constants.LogNameLog3, PathType.LogName, []); + var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); + var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); + var logData3 = new EventLogData(Constants.LogNameLog3, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); @@ -747,8 +747,8 @@ public void ReduceCloseLog_WhenThreeTables_ShouldKeepCombinedAndRemainingTables( public void ReduceCloseLog_WhenTwoTables_ShouldRemoveCombinedAndSetRemaining() { // Arrange - var logData1 = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); - var logData2 = new EventLogData(Constants.LogNameLog2, PathType.LogName, []); + var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); + var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); @@ -903,7 +903,7 @@ public void ReduceReorderColumn_WithDisabledColumnsInterleaved_ShouldInsertRelat public void ReduceSetActiveTable_WhenTableIsLoading_ShouldChangeActive() { // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); @@ -920,8 +920,8 @@ public void ReduceSetActiveTable_WhenTableIsLoading_ShouldChangeActive() public void ReduceSetActiveTable_WhenTableIsNotLoading_ShouldChangeActive() { // Arrange - var logData1 = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); - var logData2 = new EventLogData(Constants.LogNameLog2, PathType.LogName, []); + var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); + var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); @@ -999,14 +999,14 @@ public void ReduceSetOrderBy_WhenToggledOff_ShouldSortPerLogListsByEffectiveComp // (DateAndTime). Otherwise a subsequent ReduceUpdateCombinedEvents merges RecordId-sorted // inputs under a DateAndTime comparator and produces silently incorrect output. var baseTime = new DateTime(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc); - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState { OrderBy = ColumnName.Level, IsDescending = false }; state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); var events = new List { - new(Constants.LogNameTestLog, PathType.LogName) { Id = 10, RecordId = 1, TimeCreated = baseTime.AddSeconds(40) }, - new(Constants.LogNameTestLog, PathType.LogName) { Id = 11, RecordId = 2, TimeCreated = baseTime.AddSeconds(20) } + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 10, RecordId = 1, TimeCreated = baseTime.AddSeconds(40) }, + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 11, RecordId = 2, TimeCreated = baseTime.AddSeconds(20) } }; state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData.Id, events)); @@ -1057,7 +1057,7 @@ public void ReduceSetOrderBy_WithSameColumn_ShouldToggleSorting() public void ReduceToggleLoading_ShouldToggleLoadingState() { // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); @@ -1107,14 +1107,14 @@ public void ReduceToggleSorting_WhenOrderByIsNull_ShouldPreserveNullOrderBy() // the UI does not silently re-light a column header. Sort still uses the effective // (DateAndTime) comparator inside SortDisplayEvents. var baseTime = new DateTime(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc); - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState { OrderBy = null, IsDescending = false }; state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); var events = new List { - new(Constants.LogNameTestLog, PathType.LogName) { Id = 10, RecordId = 1, TimeCreated = baseTime.AddSeconds(40) }, - new(Constants.LogNameTestLog, PathType.LogName) { Id = 11, RecordId = 2, TimeCreated = baseTime.AddSeconds(20) } + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 10, RecordId = 1, TimeCreated = baseTime.AddSeconds(40) }, + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 11, RecordId = 2, TimeCreated = baseTime.AddSeconds(20) } }; state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData.Id, events)); @@ -1135,20 +1135,20 @@ public void ReduceUpdateDisplayedEvents_ShouldUpdateSlicesForLogsInActiveLogs() { // Arrange — two logs added, then a filter-driven UpdateDisplayedEvents arrives with // post-filter results for both logs. - var logData1 = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); - var logData2 = new EventLogData(Constants.LogNameLog2, PathType.LogName, []); + var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); + var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); var log1Events = new List { - new(Constants.LogNameLog1, PathType.LogName) { Id = 10, RecordId = 1 } + new(Constants.LogNameLog1, LogPathType.Channel) { Id = 10, RecordId = 1 } }; var log2Events = new List { - new(Constants.LogNameLog2, PathType.LogName) { Id = 20, RecordId = 2 } + new(Constants.LogNameLog2, LogPathType.Channel) { Id = 20, RecordId = 2 } }; var activeLogs = new Dictionary> @@ -1172,19 +1172,19 @@ public void ReduceUpdateDisplayedEvents_ShouldUpdateSlicesForLogsInActiveLogs() public void ReduceUpdateDisplayedEvents_WhenActiveLogsContainsClosedLogId_ShouldSkipIt() { // Arrange — filter result includes a log id whose tab was closed mid-flight - var openLog = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); + var openLog = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(openLog)); var staleLogId = EventLogId.Create(); var openLogEvents = new List { - new(Constants.LogNameLog1, PathType.LogName) { Id = 10, RecordId = 1 } + new(Constants.LogNameLog1, LogPathType.Channel) { Id = 10, RecordId = 1 } }; var staleEvents = new List { - new("ClosedLog", PathType.LogName) { Id = 99, RecordId = 99 } + new("ClosedLog", LogPathType.Channel) { Id = 99, RecordId = 99 } }; var activeLogs = new Dictionary> @@ -1211,14 +1211,14 @@ public void ReduceUpdateDisplayedEvents_WhenLogIsNotInActiveLogs_ShouldPreserveE // then arrives with empty ActiveLogs (e.g. log opened mid-filter so the snapshot did // not include it). The omitted log's rows must stay in canonical — otherwise a fresh // load could be silently scrubbed by a stale filter result. - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); var loadedEvents = new List { - new(Constants.LogNameTestLog, PathType.LogName) { Id = 10, RecordId = 1 }, - new(Constants.LogNameTestLog, PathType.LogName) { Id = 11, RecordId = 2 } + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 10, RecordId = 1 }, + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 11, RecordId = 2 } }; state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData.Id, loadedEvents)); @@ -1241,23 +1241,23 @@ public void ReduceUpdateDisplayedEvents_WhenSomeLogsOmitted_ShouldReplaceInclude // Arrange — two logs, both populated via UpdateTable. Filter dispatch arrives for log A // only (e.g. log B opened or finished loading after the snapshot). Log A's rows must be // replaced by the filter result; log B's rows must be preserved untouched. - var logData1 = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); - var logData2 = new EventLogData(Constants.LogNameLog2, PathType.LogName, []); + var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); + var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); var log1Loaded = new List { - new(Constants.LogNameLog1, PathType.LogName) { Id = 100, RecordId = 1 }, - new(Constants.LogNameLog1, PathType.LogName) { Id = 101, RecordId = 2 } + new(Constants.LogNameLog1, LogPathType.Channel) { Id = 100, RecordId = 1 }, + new(Constants.LogNameLog1, LogPathType.Channel) { Id = 101, RecordId = 2 } }; var log2Loaded = new List { - new(Constants.LogNameLog2, PathType.LogName) { Id = 200, RecordId = 1 }, - new(Constants.LogNameLog2, PathType.LogName) { Id = 201, RecordId = 2 }, - new(Constants.LogNameLog2, PathType.LogName) { Id = 202, RecordId = 3 } + new(Constants.LogNameLog2, LogPathType.Channel) { Id = 200, RecordId = 1 }, + new(Constants.LogNameLog2, LogPathType.Channel) { Id = 201, RecordId = 2 }, + new(Constants.LogNameLog2, LogPathType.Channel) { Id = 202, RecordId = 3 } }; state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData1.Id, log1Loaded)); @@ -1265,7 +1265,7 @@ public void ReduceUpdateDisplayedEvents_WhenSomeLogsOmitted_ShouldReplaceInclude var log1Filtered = new List { - new(Constants.LogNameLog1, PathType.LogName) { Id = 100, RecordId = 1 } + new(Constants.LogNameLog1, LogPathType.Channel) { Id = 100, RecordId = 1 } }; var activeLogs = new Dictionary> @@ -1290,14 +1290,14 @@ public void ReduceUpdateDisplayedEvents_WhenSomeLogsOmitted_ShouldReplaceInclude public void ReduceUpdateDisplayedEvents_WhenTableComputerNameEmpty_ShouldLatchFromFirstNonEmptyEvent() { // Arrange — log first becomes visible via UpdateDisplayedEvents (filter clear), not via append - var logData = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); var revealedEvents = new List { - new(Constants.LogNameLog1, PathType.LogName) { Id = 10, RecordId = 1, ComputerName = string.Empty }, - new(Constants.LogNameLog1, PathType.LogName) { Id = 11, RecordId = 2, ComputerName = Constants.EventComputerServer01 } + new(Constants.LogNameLog1, LogPathType.Channel) { Id = 10, RecordId = 1, ComputerName = string.Empty }, + new(Constants.LogNameLog1, LogPathType.Channel) { Id = 11, RecordId = 2, ComputerName = Constants.EventComputerServer01 } }; var activeLogs = new Dictionary> @@ -1319,14 +1319,14 @@ public void ReduceUpdateDisplayedEvents_WhenTableComputerNameEmpty_ShouldLatchFr public void ReduceUpdateTable_AfterPartialAppends_ShouldReplaceNotMergeWithPartials() { // Arrange — partial AppendTableEvents land first (live-load deltas), then UpdateTable arrives with the full filtered list - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState { IsDescending = false }; state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); var partial1 = new List { - new(Constants.LogNameTestLog, PathType.LogName) { Id = 10, RecordId = 1 }, - new(Constants.LogNameTestLog, PathType.LogName) { Id = 11, RecordId = 2 } + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 10, RecordId = 1 }, + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 11, RecordId = 2 } }; state = EventTableReducers.ReduceAppendTableEvents( @@ -1335,7 +1335,7 @@ public void ReduceUpdateTable_AfterPartialAppends_ShouldReplaceNotMergeWithParti var partial2 = new List { - new(Constants.LogNameTestLog, PathType.LogName) { Id = 12, RecordId = 3 } + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 12, RecordId = 3 } }; state = EventTableReducers.ReduceAppendTableEvents( @@ -1346,10 +1346,10 @@ public void ReduceUpdateTable_AfterPartialAppends_ShouldReplaceNotMergeWithParti var fullLoad = new List { - new(Constants.LogNameTestLog, PathType.LogName) { Id = 20, RecordId = 1 }, - new(Constants.LogNameTestLog, PathType.LogName) { Id = 21, RecordId = 2 }, - new(Constants.LogNameTestLog, PathType.LogName) { Id = 22, RecordId = 3 }, - new(Constants.LogNameTestLog, PathType.LogName) { Id = 23, RecordId = 4 } + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 20, RecordId = 1 }, + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 21, RecordId = 2 }, + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 22, RecordId = 3 }, + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 23, RecordId = 4 } }; // Act @@ -1367,7 +1367,7 @@ public void ReduceUpdateTable_AfterPartialAppends_ShouldReplaceNotMergeWithParti public void ReduceUpdateTable_ShouldUpdateTableEvents() { // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); @@ -1393,14 +1393,14 @@ public void ReduceUpdateTable_ShouldUpdateTableEvents() public void ReduceUpdateTable_WhenCalledTwiceForSameLog_ShouldReplaceNotDuplicate() { // Arrange — first UpdateTable populates canonical with two events - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, []); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState { IsDescending = false }; state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); var firstLoad = new List { - new(Constants.LogNameTestLog, PathType.LogName) { Id = 10, RecordId = 1 }, - new(Constants.LogNameTestLog, PathType.LogName) { Id = 11, RecordId = 2 } + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 10, RecordId = 1 }, + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 11, RecordId = 2 } }; state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData.Id, firstLoad)); @@ -1408,9 +1408,9 @@ public void ReduceUpdateTable_WhenCalledTwiceForSameLog_ShouldReplaceNotDuplicat var secondLoad = new List { - new(Constants.LogNameTestLog, PathType.LogName) { Id = 12, RecordId = 3 }, - new(Constants.LogNameTestLog, PathType.LogName) { Id = 13, RecordId = 4 }, - new(Constants.LogNameTestLog, PathType.LogName) { Id = 14, RecordId = 5 } + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 12, RecordId = 3 }, + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 13, RecordId = 4 }, + new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 14, RecordId = 5 } }; // Act — second UpdateTable for the same log (e.g., filter-driven reload) @@ -1426,22 +1426,22 @@ public void ReduceUpdateTable_WhenCalledTwiceForSameLog_ShouldReplaceNotDuplicat public void ReduceUpdateTable_WhenDescendingOrderRequested_ShouldMergeInDescendingOrder() { // Arrange — two logs, descending sort - var logData1 = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); - var logData2 = new EventLogData(Constants.LogNameLog2, PathType.LogName, []); + var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); + var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); var state = new EventTableState { IsDescending = true }; state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); var eventsLog1 = new List { - new(Constants.LogNameLog1, PathType.LogName) { Id = 10, RecordId = 1 }, - new(Constants.LogNameLog1, PathType.LogName) { Id = 11, RecordId = 3 } + new(Constants.LogNameLog1, LogPathType.Channel) { Id = 10, RecordId = 1 }, + new(Constants.LogNameLog1, LogPathType.Channel) { Id = 11, RecordId = 3 } }; var eventsLog2 = new List { - new(Constants.LogNameLog2, PathType.LogName) { Id = 20, RecordId = 2 }, - new(Constants.LogNameLog2, PathType.LogName) { Id = 21, RecordId = 4 } + new(Constants.LogNameLog2, LogPathType.Channel) { Id = 20, RecordId = 2 }, + new(Constants.LogNameLog2, LogPathType.Channel) { Id = 21, RecordId = 4 } }; // Act @@ -1460,16 +1460,16 @@ public void ReduceUpdateTable_WhenDescendingOrderRequested_ShouldMergeInDescendi public void ReduceUpdateTable_WhenSecondLogIsEmpty_ShouldKeepFirstLogEvents() { // Arrange — one log populated, one log empty (but not loading), ascending RecordId order - var logData1 = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); - var logData2 = new EventLogData(Constants.LogNameLog2, PathType.LogName, []); + var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); + var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); var state = new EventTableState { IsDescending = false }; state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); var eventsLog1 = new List { - new(Constants.LogNameLog1, PathType.LogName) { Id = 10, RecordId = 5 }, - new(Constants.LogNameLog1, PathType.LogName) { Id = 11, RecordId = 7 } + new(Constants.LogNameLog1, LogPathType.Channel) { Id = 10, RecordId = 5 }, + new(Constants.LogNameLog1, LogPathType.Channel) { Id = 11, RecordId = 7 } }; // Act @@ -1486,22 +1486,22 @@ public void ReduceUpdateTable_WhenSecondLogIsEmpty_ShouldKeepFirstLogEvents() public void ReduceUpdateTable_WhenSecondLogPopulated_ShouldMergeIntoCanonicalInSortedOrder() { // Arrange — two logs with interleaved RecordIds, ascending RecordId order - var logData1 = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); - var logData2 = new EventLogData(Constants.LogNameLog2, PathType.LogName, []); + var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); + var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); var state = new EventTableState { IsDescending = false }; state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); var eventsLog1 = new List { - new(Constants.LogNameLog1, PathType.LogName) { Id = 10, RecordId = 1 }, - new(Constants.LogNameLog1, PathType.LogName) { Id = 11, RecordId = 3 } + new(Constants.LogNameLog1, LogPathType.Channel) { Id = 10, RecordId = 1 }, + new(Constants.LogNameLog1, LogPathType.Channel) { Id = 11, RecordId = 3 } }; var eventsLog2 = new List { - new(Constants.LogNameLog2, PathType.LogName) { Id = 20, RecordId = 2 }, - new(Constants.LogNameLog2, PathType.LogName) { Id = 21, RecordId = 4 } + new(Constants.LogNameLog2, LogPathType.Channel) { Id = 20, RecordId = 2 }, + new(Constants.LogNameLog2, LogPathType.Channel) { Id = 21, RecordId = 4 } }; // Act — UpdateTable maintains the canonical view atomically; no follow-up call needed. @@ -1542,18 +1542,18 @@ public void ReduceUpdateTable_WhenTimeCreatedDivergesFromRecordId_ShouldMergeByT var log1Events = new List { - new(Constants.LogNameLog1, PathType.LogName) { Id = 10, RecordId = 1, TimeCreated = baseTime.AddSeconds(40) }, - new(Constants.LogNameLog1, PathType.LogName) { Id = 11, RecordId = 2, TimeCreated = baseTime.AddSeconds(20) } + new(Constants.LogNameLog1, LogPathType.Channel) { Id = 10, RecordId = 1, TimeCreated = baseTime.AddSeconds(40) }, + new(Constants.LogNameLog1, LogPathType.Channel) { Id = 11, RecordId = 2, TimeCreated = baseTime.AddSeconds(20) } }; var log2Events = new List { - new(Constants.LogNameLog2, PathType.LogName) { Id = 20, RecordId = 1, TimeCreated = baseTime.AddSeconds(30) }, - new(Constants.LogNameLog2, PathType.LogName) { Id = 21, RecordId = 2, TimeCreated = baseTime.AddSeconds(10) } + new(Constants.LogNameLog2, LogPathType.Channel) { Id = 20, RecordId = 1, TimeCreated = baseTime.AddSeconds(30) }, + new(Constants.LogNameLog2, LogPathType.Channel) { Id = 21, RecordId = 2, TimeCreated = baseTime.AddSeconds(10) } }; - var logData1 = new EventLogData(Constants.LogNameLog1, PathType.LogName, []); - var logData2 = new EventLogData(Constants.LogNameLog2, PathType.LogName, []); + var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); + var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); var state = new EventTableState { IsDescending = false }; state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/FilterPane/FilterPaneEffectsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/FilterPane/FilterPaneEffectsTests.cs index b7ffef71..ce7c489b 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/FilterPane/FilterPaneEffectsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/FilterPane/FilterPaneEffectsTests.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; -using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Common.Channels; +using EventLogExpert.Eventing.Common.Events; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.FilterCache; @@ -200,54 +200,24 @@ public async Task HandleSaveFilterGroup_WithMultipleFilters_ShouldSaveAll() } [Fact] - public async Task HandleSetFilter_ShouldUpdateEventTableFilters() + public async Task HandleSetFilterDateRangeSuccess_ShouldUpdateEventTableFilters() { // Arrange - var filterModel = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100, isEnabled: true); - - var (effects, mockDispatcher) = CreateEffects(true, ImmutableList.Create(filterModel)); - var action = new FilterPaneAction.SetFilter(filterModel); + var (effects, mockDispatcher) = CreateEffects( + isEnabled: true, + filteredDateRange: new FilterDateModel + { + After = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), + Before = new DateTime(2024, 1, 2, 0, 0, 0, DateTimeKind.Utc) + }); // Act - await effects.HandleSetFilter(action, mockDispatcher); + await effects.HandleSetFilterDateRangeSuccess(mockDispatcher); // Assert mockDispatcher.Received(1).Dispatch(Arg.Any()); } - [Fact] - public async Task HandleSetFilter_WhenFilterIsCached_ShouldNotAddToRecentFilters() - { - // Arrange - var filterModel = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100, FilterType.Cached); - - var (effects, mockDispatcher) = CreateEffects(); - var action = new FilterPaneAction.SetFilter(filterModel); - - // Act - await effects.HandleSetFilter(action, mockDispatcher); - - // Assert - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); - } - - [Fact] - public async Task HandleSetFilter_WhenFilterIsNotCached_ShouldAddToRecentFilters() - { - // Arrange - var filterModel = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100, FilterType.Advanced); - - var (effects, mockDispatcher) = CreateEffects(); - var action = new FilterPaneAction.SetFilter(filterModel); - - // Act - await effects.HandleSetFilter(action, mockDispatcher); - - // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => - x.Filter == Constants.FilterIdEquals100)); - } - [Fact] public async Task HandleSetFilterDateRange_WhenAfterIsNull_ShouldUseEnvelopeFromActiveLogs() { @@ -262,7 +232,7 @@ public async Task HandleSetFilterDateRange_WhenAfterIsNull_ShouldUseEnvelopeFrom EventUtils.CreateTestEvent(timeCreated: oldest) }; - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, events); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, events); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); var (effects, mockDispatcher) = CreateEffects(activeLogs: activeLogs); @@ -293,7 +263,7 @@ public async Task HandleSetFilterDateRange_WhenBeforeIsNull_ShouldUseEnvelopeFro EventUtils.CreateTestEvent(timeCreated: oldest) }; - var logData = new EventLogData(Constants.LogNameTestLog, PathType.LogName, events); + var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, events); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); var (effects, mockDispatcher) = CreateEffects(activeLogs: activeLogs); @@ -322,11 +292,11 @@ public async Task HandleSetFilterDateRange_WhenBothNullAcrossMultipleLogs_Should var logA = new EventLogData( "LogA", - PathType.LogName, + LogPathType.Channel, [EventUtils.CreateTestEvent(timeCreated: logANewest), EventUtils.CreateTestEvent(timeCreated: logAOldest)]); var logB = new EventLogData( "LogB", - PathType.LogName, + LogPathType.Channel, [EventUtils.CreateTestEvent(timeCreated: logBNewest), EventUtils.CreateTestEvent(timeCreated: logBOldest)]); var activeLogs = ImmutableDictionary.Empty @@ -426,24 +396,54 @@ public async Task HandleSetFilterDateRange_WhenFilterDateModelIsNull_ShouldDispa } [Fact] - public async Task HandleSetFilterDateRangeSuccess_ShouldUpdateEventTableFilters() + public async Task HandleSetFilter_ShouldUpdateEventTableFilters() { // Arrange - var (effects, mockDispatcher) = CreateEffects( - isEnabled: true, - filteredDateRange: new FilterDateModel - { - After = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), - Before = new DateTime(2024, 1, 2, 0, 0, 0, DateTimeKind.Utc) - }); + var filterModel = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100, isEnabled: true); + + var (effects, mockDispatcher) = CreateEffects(true, ImmutableList.Create(filterModel)); + var action = new FilterPaneAction.SetFilter(filterModel); // Act - await effects.HandleSetFilterDateRangeSuccess(mockDispatcher); + await effects.HandleSetFilter(action, mockDispatcher); // Assert mockDispatcher.Received(1).Dispatch(Arg.Any()); } + [Fact] + public async Task HandleSetFilter_WhenFilterIsCached_ShouldNotAddToRecentFilters() + { + // Arrange + var filterModel = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100, FilterType.Cached); + + var (effects, mockDispatcher) = CreateEffects(); + var action = new FilterPaneAction.SetFilter(filterModel); + + // Act + await effects.HandleSetFilter(action, mockDispatcher); + + // Assert + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + } + + [Fact] + public async Task HandleSetFilter_WhenFilterIsNotCached_ShouldAddToRecentFilters() + { + // Arrange + var filterModel = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100, FilterType.Advanced); + + var (effects, mockDispatcher) = CreateEffects(); + var action = new FilterPaneAction.SetFilter(filterModel); + + // Act + await effects.HandleSetFilter(action, mockDispatcher); + + // Assert + mockDispatcher.Received(1).Dispatch(Arg.Is(x => + x.Filter == Constants.FilterIdEquals100)); + } + [Fact] public async Task HandleToggleFilterDate_ShouldUpdateEventTableFilters() { diff --git a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/DatabaseSeedUtils.cs b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/DatabaseSeedUtils.cs index 2dddfbf2..eee2bfeb 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/DatabaseSeedUtils.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/DatabaseSeedUtils.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.EventProviderDatabase; using EventLogExpert.Eventing.Logging; +using EventLogExpert.Eventing.ProviderDatabase; using Microsoft.Data.Sqlite; namespace EventLogExpert.UI.Tests.TestUtils; @@ -76,7 +76,7 @@ internal static void SeedV3Schema(string dbPath) internal static void SeedV4Schema(string dbPath, ITraceLogger? logger = null) { - using (var context = new EventProviderDbContext(dbPath, readOnly: false, ensureCreated: true, logger)) + using (var context = new ProviderDbContext(dbPath, readOnly: false, ensureCreated: true, logger)) { context.Database.EnsureCreated(); } diff --git a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/EventUtils.cs b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/EventUtils.cs index 8a5ca00f..94c41601 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/EventUtils.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/EventUtils.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; -using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Common.Channels; +using EventLogExpert.Eventing.Common.Events; namespace EventLogExpert.UI.Tests.TestUtils; @@ -22,7 +22,7 @@ internal static DisplayEventModel CreateTestEvent( int? processId = null, int? threadId = null, IReadOnlyList? keywords = null) => - new("TestLog", PathType.LogName) + new("TestLog", LogPathType.Channel) { Id = id, Source = source, diff --git a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/LoggerUtils.cs b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/LoggerUtils.cs index 6c0dc7e1..442dcb7b 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/LoggerUtils.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/LoggerUtils.cs @@ -30,10 +30,10 @@ internal sealed class RecordingTraceLogger : ITraceLogger public void Error(ErrorLogHandler handler) => ErrorMessages.Add(handler.ToStringAndClear()); - public void Info(InfoLogHandler handler) => InfoMessages.Add(handler.ToStringAndClear()); + public void Information(InformationLogHandler handler) => InfoMessages.Add(handler.ToStringAndClear()); public void Trace(TraceLogHandler handler) => TraceMessages.Add(handler.ToStringAndClear()); - public void Warn(WarnLogHandler handler) => WarnMessages.Add(handler.ToStringAndClear()); + public void Warning(WarningLogHandler handler) => WarnMessages.Add(handler.ToStringAndClear()); } } From c8ce6d0e8639a9a8be73cd1770c1f88fb294fd73 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sat, 9 May 2026 18:38:52 -0500 Subject: [PATCH 2/2] Rename DisplayEventModel to ResolvedEvent --- .../Common/Channels/LogPathType.cs | 2 +- ...{DisplayEventModel.cs => ResolvedEvent.cs} | 4 +- .../Providers/EventMessageProvider.cs | 40 +++++--- .../Providers/RegistryProvider.cs | 2 +- .../Readers/EventLogWatcher.cs | 1 + .../Resolvers/EventResolver.cs | 6 +- .../Resolvers/EventResolverBase.cs | 4 +- .../Resolvers/EventXmlResolver.cs | 2 +- .../Resolvers/IEventResolver.cs | 4 +- .../Resolvers/IEventXmlResolver.cs | 8 +- src/EventLogExpert.UI/Enums.cs | 6 +- src/EventLogExpert.UI/FilterMethods.cs | 92 ++++++++--------- .../Interfaces/IFilterService.cs | 6 +- .../Models/CompiledFilter.cs | 6 +- src/EventLogExpert.UI/Models/EventFilter.cs | 4 +- src/EventLogExpert.UI/Models/EventLogData.cs | 4 +- .../Services/FilterCompiler.cs | 6 +- .../Services/FilterService.cs | 26 ++--- .../Store/EventLog/EventLogAction.cs | 16 +-- .../Store/EventLog/EventLogEffects.cs | 22 ++--- .../Store/EventLog/EventLogReducers.cs | 32 +++--- .../Store/EventLog/EventLogState.cs | 6 +- .../Store/EventTable/EventTableAction.cs | 10 +- .../Store/EventTable/EventTableReducers.cs | 18 ++-- .../Store/EventTable/EventTableState.cs | 2 +- .../Components/Sections/DetailsPane.razor.cs | 6 +- .../Components/Sections/EventTable.razor | 4 +- .../Components/Sections/EventTable.razor.cs | 88 ++++++++--------- .../Sections/SplitLogTabPane.razor.cs | 9 +- .../Services/ClipboardService.cs | 14 +-- .../Services/MauiMenuActionService.cs | 41 ++++---- .../Resolvers/EventXmlResolverTests.cs | 10 +- .../Resolvers/VersatileEventResolverTests.cs | 10 +- .../DateRangeDefaultsTests.cs | 2 +- .../FilterMethodsTests.cs | 40 ++++---- .../Services/FilterCategoryItemsCacheTests.cs | 2 +- .../Services/FilterServiceTests.cs | 8 +- .../Store/EventLog/EventLogEffectsTests.cs | 76 +++++++------- .../Store/EventLog/EventLogStoreTests.cs | 22 ++--- .../Store/EventTable/EventTableStoreTests.cs | 98 +++++++++---------- .../FilterPane/FilterPaneEffectsTests.cs | 4 +- .../TestUtils/EventUtils.cs | 2 +- 42 files changed, 392 insertions(+), 373 deletions(-) rename src/EventLogExpert.Eventing/Common/Events/{DisplayEventModel.cs => ResolvedEvent.cs} (91%) diff --git a/src/EventLogExpert.Eventing/Common/Channels/LogPathType.cs b/src/EventLogExpert.Eventing/Common/Channels/LogPathType.cs index 5f66d190..8375bb6b 100644 --- a/src/EventLogExpert.Eventing/Common/Channels/LogPathType.cs +++ b/src/EventLogExpert.Eventing/Common/Channels/LogPathType.cs @@ -6,5 +6,5 @@ namespace EventLogExpert.Eventing.Common.Channels; public enum LogPathType { Channel = 1, - File + File = 2 } diff --git a/src/EventLogExpert.Eventing/Common/Events/DisplayEventModel.cs b/src/EventLogExpert.Eventing/Common/Events/ResolvedEvent.cs similarity index 91% rename from src/EventLogExpert.Eventing/Common/Events/DisplayEventModel.cs rename to src/EventLogExpert.Eventing/Common/Events/ResolvedEvent.cs index 11bf8a58..a71f03f1 100644 --- a/src/EventLogExpert.Eventing/Common/Events/DisplayEventModel.cs +++ b/src/EventLogExpert.Eventing/Common/Events/ResolvedEvent.cs @@ -6,7 +6,7 @@ namespace EventLogExpert.Eventing.Common.Events; -public sealed record DisplayEventModel( +public sealed record ResolvedEvent( string OwningLog /*This is the name of the log file or the live log, which we use internally*/, LogPathType LogPathType) { @@ -45,7 +45,7 @@ public sealed record DisplayEventModel( /// Pre-rendered XML for the event. Populated by EventLogReader only when the /// log is opened with renderXml: true (currently driven by the presence of an /// applied filter that references this property). When empty, callers should use - /// to fetch the XML on demand. + /// to fetch the XML on demand. ///
public string Xml { get; init; } = string.Empty; } diff --git a/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs b/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs index 632c6450..5d7aceb4 100644 --- a/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs +++ b/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs @@ -319,7 +319,7 @@ private ProviderDetails LoadProviderDetailsCore(HashSet? visited) var legacyProviderFiles = _registryProvider.GetMessageFilesForLegacyProvider(_providerName); - if (legacyProviderFiles.Any() && + if (legacyProviderFiles.Count > 0 && TryLoadMessages(legacyProviderFiles, out var legacyMessages) && legacyMessages.Count > 0) { @@ -327,11 +327,23 @@ private ProviderDetails LoadProviderDetailsCore(HashSet? visited) } else if (!string.IsNullOrEmpty(providerMetadata?.MessageFilePath)) { - _logger?.Debug( - $"No legacy messages loaded for provider {_providerName}. Using message file from modern provider."); + if (TryLoadMessages([providerMetadata.MessageFilePath], out var modernMessages) && modernMessages.Count > 0) + { + _logger?.Debug( + $"No legacy messages loaded for provider {_providerName}. Using message file from modern provider."); - _ = TryLoadMessages([providerMetadata.MessageFilePath], out var modernMessages); - provider.Messages = modernMessages; + provider.Messages = modernMessages; + } + else + { + _logger?.Debug( + $"No legacy messages loaded for provider {_providerName} and modern message file fallback produced no messages. Returning empty provider details."); + } + } + else if (legacyProviderFiles.Count > 0) + { + _logger?.Debug( + $"Legacy message files for provider {_providerName} produced no messages and no modern fallback is available. Returning empty provider details."); } else { @@ -340,8 +352,15 @@ private ProviderDetails LoadProviderDetailsCore(HashSet? visited) if (!string.IsNullOrEmpty(providerMetadata?.ParameterFilePath)) { - _ = TryLoadMessages([providerMetadata.ParameterFilePath], out var parameterMessages); - provider.Parameters = parameterMessages; + if (TryLoadMessages([providerMetadata.ParameterFilePath], out var parameterMessages) && parameterMessages.Count > 0) + { + provider.Parameters = parameterMessages; + } + else + { + _logger?.Debug( + $"Parameter file fallback for provider {_providerName} produced no messages."); + } } if (provider.IsEmpty) @@ -503,11 +522,6 @@ and not StackOverflowException } } - /// - /// Tries to load messages for a legacy provider from the registry-specified files - /// (HKLM\SYSTEM\CurrentControlSet\Services\EventLog). Returns false on any failure so the caller can fall back to - /// modern provider data instead of bubbling the exception. - /// private bool TryLoadMessages(IEnumerable messageFilePaths, out List messages) { _logger?.Debug($"{nameof(TryLoadMessages)} called for files {string.Join(", ", messageFilePaths)}"); @@ -522,7 +536,7 @@ private bool TryLoadMessages(IEnumerable messageFilePaths, out List - public IEnumerable GetMessageFilesForLegacyProvider(string providerName) + public IReadOnlyList GetMessageFilesForLegacyProvider(string providerName) { _logger?.Debug($"{nameof(GetMessageFilesForLegacyProvider)} called for provider {providerName}"); diff --git a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs index 1a0f7a4f..66feb96a 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs @@ -174,6 +174,7 @@ private void ReadAndRaiseEvents(object? state, bool timedOut) } @event.PathName = _path; + @event.LogPathType = LogPathType.Channel; var subscribers = EventRecordWritten; diff --git a/src/EventLogExpert.Eventing/Resolvers/EventResolver.cs b/src/EventLogExpert.Eventing/Resolvers/EventResolver.cs index ecb2fe1f..95b9190a 100644 --- a/src/EventLogExpert.Eventing/Resolvers/EventResolver.cs +++ b/src/EventLogExpert.Eventing/Resolvers/EventResolver.cs @@ -44,7 +44,7 @@ public EventResolver( public void LoadProviderDetails(EventRecord eventRecord) { - ObjectDisposedException.ThrowIf(IsDisposed, nameof(EventResolver)); + ObjectDisposedException.ThrowIf(IsDisposed, this); var providerName = eventRecord.ProviderName; @@ -80,7 +80,7 @@ public void LoadProviderDetails(EventRecord eventRecord) } } - public override DisplayEventModel ResolveEvent(EventRecord eventRecord) + public override ResolvedEvent ResolveEvent(EventRecord eventRecord) { LoadProviderDetails(eventRecord); @@ -246,7 +246,7 @@ private bool TryResolveFromDatabase(string providerName) { using (_databaseAccessLock.EnterScope()) { - ObjectDisposedException.ThrowIf(IsDisposed, nameof(EventResolver)); + ObjectDisposedException.ThrowIf(IsDisposed, this); foreach (var dbContext in _dbContexts) { diff --git a/src/EventLogExpert.Eventing/Resolvers/EventResolverBase.cs b/src/EventLogExpert.Eventing/Resolvers/EventResolverBase.cs index d7ab44f3..b34b3034 100644 --- a/src/EventLogExpert.Eventing/Resolvers/EventResolverBase.cs +++ b/src/EventLogExpert.Eventing/Resolvers/EventResolverBase.cs @@ -76,7 +76,7 @@ public void Dispose() GC.SuppressFinalize(this); } - public virtual DisplayEventModel ResolveEvent(EventRecord eventRecord) + public virtual ResolvedEvent ResolveEvent(EventRecord eventRecord) { ObjectDisposedException.ThrowIf(IsDisposed, this); @@ -467,7 +467,7 @@ private int CountVisibleTemplateProperties(ReadOnlySpan template) return visibleCount; } - private DisplayEventModel CreateEventModel( + private ResolvedEvent CreateEventModel( EventRecord eventRecord, EventModel? modernEvent, ProviderDetails? details, diff --git a/src/EventLogExpert.Eventing/Resolvers/EventXmlResolver.cs b/src/EventLogExpert.Eventing/Resolvers/EventXmlResolver.cs index 9571aa62..b44a0ad4 100644 --- a/src/EventLogExpert.Eventing/Resolvers/EventXmlResolver.cs +++ b/src/EventLogExpert.Eventing/Resolvers/EventXmlResolver.cs @@ -81,7 +81,7 @@ public void ClearXmlCacheForLog(string owningLog) } } - public ValueTask GetXmlAsync(DisplayEventModel evt, CancellationToken cancellationToken = default) + public ValueTask GetXmlAsync(ResolvedEvent evt, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(evt); diff --git a/src/EventLogExpert.Eventing/Resolvers/IEventResolver.cs b/src/EventLogExpert.Eventing/Resolvers/IEventResolver.cs index 32370efc..8a8f090f 100644 --- a/src/EventLogExpert.Eventing/Resolvers/IEventResolver.cs +++ b/src/EventLogExpert.Eventing/Resolvers/IEventResolver.cs @@ -1,4 +1,4 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.Eventing.Common.Events; @@ -11,7 +11,7 @@ public interface IEventResolver : IDisposable { void LoadProviderDetails(EventRecord eventRecord); - DisplayEventModel ResolveEvent(EventRecord eventRecord); + ResolvedEvent ResolveEvent(EventRecord eventRecord); void SetMetadataPaths(IReadOnlyList metadataPaths); } diff --git a/src/EventLogExpert.Eventing/Resolvers/IEventXmlResolver.cs b/src/EventLogExpert.Eventing/Resolvers/IEventXmlResolver.cs index 490ea3ec..6eaf7408 100644 --- a/src/EventLogExpert.Eventing/Resolvers/IEventXmlResolver.cs +++ b/src/EventLogExpert.Eventing/Resolvers/IEventXmlResolver.cs @@ -6,7 +6,7 @@ namespace EventLogExpert.Eventing.Resolvers; /// -/// Resolves the raw XML for a on demand and caches the result. Implementations +/// Resolves the raw XML for a on demand and caches the result. Implementations /// must be thread-safe and must coalesce concurrent requests for the same event into a single underlying /// EvtQuery / RenderEventXml call. /// @@ -19,10 +19,10 @@ public interface IEventXmlResolver void ClearXmlCacheForLog(string owningLog); /// - /// Returns the XML for . If is already populated + /// Returns the XML for . If is already populated /// (because the log was opened with renderXml: true), the pre-rendered value is returned immediately; otherwise /// the resolver re-opens the source log via EvtQuery, locates the record by - /// , and renders the XML. + /// , and renders the XML. /// /// The event to resolve XML for. /// @@ -30,5 +30,5 @@ public interface IEventXmlResolver /// in-flight resolution; concurrent callers waiting on the same cache entry continue to observe the result. /// /// The XML string, or if the record cannot be located or rendering fails. - ValueTask GetXmlAsync(DisplayEventModel evt, CancellationToken cancellationToken = default); + ValueTask GetXmlAsync(ResolvedEvent evt, CancellationToken cancellationToken = default); } diff --git a/src/EventLogExpert.UI/Enums.cs b/src/EventLogExpert.UI/Enums.cs index d69d2bc2..5ddc03b5 100644 --- a/src/EventLogExpert.UI/Enums.cs +++ b/src/EventLogExpert.UI/Enums.cs @@ -111,8 +111,10 @@ public enum FilterType public enum OpenLogStatus { - Loaded, - Empty + Opened, + Empty, + Skipped, + Failed } public enum Theme diff --git a/src/EventLogExpert.UI/FilterMethods.cs b/src/EventLogExpert.UI/FilterMethods.cs index 3ef39384..23139d35 100644 --- a/src/EventLogExpert.UI/FilterMethods.cs +++ b/src/EventLogExpert.UI/FilterMethods.cs @@ -1,4 +1,4 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.Eventing.Common.Events; @@ -8,47 +8,47 @@ namespace EventLogExpert.UI; public static class FilterMethods { - private static readonly Comparison s_ascByLevel = + private static readonly Comparison s_ascByLevel = (a, b) => WithTieBreaker(string.Compare(a.Level, b.Level, StringComparison.Ordinal), a, b); - private static readonly Comparison s_ascByDateAndTime = + private static readonly Comparison s_ascByDateAndTime = (a, b) => WithTieBreaker(a.TimeCreated.CompareTo(b.TimeCreated), a, b); - private static readonly Comparison s_ascByActivityId = + private static readonly Comparison s_ascByActivityId = (a, b) => WithTieBreaker(Nullable.Compare(a.ActivityId, b.ActivityId), a, b); - private static readonly Comparison s_ascByLog = + private static readonly Comparison s_ascByLog = (a, b) => WithTieBreaker(string.Compare(a.LogName, b.LogName, StringComparison.Ordinal), a, b); - private static readonly Comparison s_ascByComputerName = + private static readonly Comparison s_ascByComputerName = (a, b) => WithTieBreaker(string.Compare(a.ComputerName, b.ComputerName, StringComparison.Ordinal), a, b); - private static readonly Comparison s_ascBySource = + private static readonly Comparison s_ascBySource = (a, b) => WithTieBreaker(string.Compare(a.Source, b.Source, StringComparison.Ordinal), a, b); - private static readonly Comparison s_ascByEventId = + private static readonly Comparison s_ascByEventId = (a, b) => WithTieBreaker(a.Id.CompareTo(b.Id), a, b); - private static readonly Comparison s_ascByTaskCategory = + private static readonly Comparison s_ascByTaskCategory = (a, b) => WithTieBreaker(string.Compare(a.TaskCategory, b.TaskCategory, StringComparison.Ordinal), a, b); - private static readonly Comparison s_ascByKeywords = + private static readonly Comparison s_ascByKeywords = (a, b) => WithTieBreaker(string.Compare(a.KeywordsDisplayName, b.KeywordsDisplayName, StringComparison.Ordinal), a, b); - private static readonly Comparison s_ascByProcessId = + private static readonly Comparison s_ascByProcessId = (a, b) => WithTieBreaker(Nullable.Compare(a.ProcessId, b.ProcessId), a, b); - private static readonly Comparison s_ascByThreadId = + private static readonly Comparison s_ascByThreadId = (a, b) => WithTieBreaker(Nullable.Compare(a.ThreadId, b.ThreadId), a, b); - private static readonly Comparison s_ascByUser = + private static readonly Comparison s_ascByUser = (a, b) => WithTieBreaker(string.Compare(a.UserId?.Value, b.UserId?.Value, StringComparison.Ordinal), a, b); - private static readonly Comparison s_ascByDefault = + private static readonly Comparison s_ascByDefault = (a, b) => FallbackTieBreaker(Nullable.Compare(a.RecordId, b.RecordId), a, b); - private static readonly Comparison s_descByActivityId = (a, b) => s_ascByActivityId(b, a); - private static readonly Comparison s_descByComputerName = (a, b) => s_ascByComputerName(b, a); - private static readonly Comparison s_descByDateAndTime = (a, b) => s_ascByDateAndTime(b, a); - private static readonly Comparison s_descByDefault = (a, b) => s_ascByDefault(b, a); - private static readonly Comparison s_descByEventId = (a, b) => s_ascByEventId(b, a); - private static readonly Comparison s_descByKeywords = (a, b) => s_ascByKeywords(b, a); - private static readonly Comparison s_descByLevel = (a, b) => s_ascByLevel(b, a); - private static readonly Comparison s_descByLog = (a, b) => s_ascByLog(b, a); - private static readonly Comparison s_descByProcessId = (a, b) => s_ascByProcessId(b, a); - private static readonly Comparison s_descBySource = (a, b) => s_ascBySource(b, a); - private static readonly Comparison s_descByTaskCategory = (a, b) => s_ascByTaskCategory(b, a); - private static readonly Comparison s_descByThreadId = (a, b) => s_ascByThreadId(b, a); - private static readonly Comparison s_descByUser = (a, b) => s_ascByUser(b, a); + private static readonly Comparison s_descByActivityId = (a, b) => s_ascByActivityId(b, a); + private static readonly Comparison s_descByComputerName = (a, b) => s_ascByComputerName(b, a); + private static readonly Comparison s_descByDateAndTime = (a, b) => s_ascByDateAndTime(b, a); + private static readonly Comparison s_descByDefault = (a, b) => s_ascByDefault(b, a); + private static readonly Comparison s_descByEventId = (a, b) => s_ascByEventId(b, a); + private static readonly Comparison s_descByKeywords = (a, b) => s_ascByKeywords(b, a); + private static readonly Comparison s_descByLevel = (a, b) => s_ascByLevel(b, a); + private static readonly Comparison s_descByLog = (a, b) => s_ascByLog(b, a); + private static readonly Comparison s_descByProcessId = (a, b) => s_ascByProcessId(b, a); + private static readonly Comparison s_descBySource = (a, b) => s_ascBySource(b, a); + private static readonly Comparison s_descByTaskCategory = (a, b) => s_ascByTaskCategory(b, a); + private static readonly Comparison s_descByThreadId = (a, b) => s_ascByThreadId(b, a); + private static readonly Comparison s_descByUser = (a, b) => s_ascByUser(b, a); public static Dictionary AddFilterGroup( this Dictionary group, @@ -126,18 +126,18 @@ public static bool IsFilteringEnabled(EventFilter eventFilter) => eventFilter.Filters.IsEmpty is false; /// Sorts events by RecordId if no order is specified. Always returns a new list. - public static IReadOnlyList SortEvents( - this IEnumerable events, + public static IReadOnlyList SortEvents( + this IEnumerable events, ColumnName? orderBy = null, bool isDescending = false) { - var sorted = new List(events); + var sorted = new List(events); sorted.Sort(GetComparer(orderBy, isDescending)); return sorted.AsReadOnly(); } - internal static Comparison GetComparer(ColumnName? orderBy, bool isDescending) => + internal static Comparison GetComparer(ColumnName? orderBy, bool isDescending) => isDescending ? orderBy switch { @@ -172,8 +172,8 @@ internal static Comparison GetComparer(ColumnName? orderBy, b _ => s_ascByDefault }; - internal static IReadOnlyList MergeSorted( - IReadOnlyList> sortedLists, + internal static IReadOnlyList MergeSorted( + IReadOnlyList> sortedLists, ColumnName orderBy, bool isDescending) { @@ -190,10 +190,10 @@ internal static IReadOnlyList MergeSorted( if (totalCount == 0) { return []; } var comparer = GetComparer(orderBy, isDescending); - var result = new List(totalCount); - var heap = new PriorityQueue( + var result = new List(totalCount); + var heap = new PriorityQueue( sortedLists.Count, - Comparer.Create(comparer)); + Comparer.Create(comparer)); var positions = new int[sortedLists.Count]; for (int listIndex = 0; listIndex < sortedLists.Count; listIndex++) @@ -204,7 +204,7 @@ internal static IReadOnlyList MergeSorted( positions[listIndex] = 1; } - while (heap.TryDequeue(out int sourceListIndex, out DisplayEventModel? currentEvent)) + while (heap.TryDequeue(out int sourceListIndex, out ResolvedEvent? currentEvent)) { result.Add(currentEvent); @@ -219,9 +219,9 @@ internal static IReadOnlyList MergeSorted( return result.AsReadOnly(); } - internal static IReadOnlyList MergeSorted( - IReadOnlyList existing, - IReadOnlyList batch, + internal static IReadOnlyList MergeSorted( + IReadOnlyList existing, + IReadOnlyList batch, ColumnName? orderBy, bool isDescending) { @@ -231,10 +231,10 @@ internal static IReadOnlyList MergeSorted( var comparer = GetComparer(orderBy, isDescending); - var sortedBatch = new List(batch); + var sortedBatch = new List(batch); sortedBatch.Sort(comparer); - var result = new List(existing.Count + sortedBatch.Count); + var result = new List(existing.Count + sortedBatch.Count); int i = 0, j = 0; while (i < existing.Count && j < sortedBatch.Count) @@ -250,13 +250,13 @@ internal static IReadOnlyList MergeSorted( } /// Falls back to RecordId, then OwningLog (for combined logs) to guarantee a total order for List.Sort stability. - private static int FallbackTieBreaker(int recordIdResult, DisplayEventModel a, DisplayEventModel b) => + private static int FallbackTieBreaker(int recordIdResult, ResolvedEvent a, ResolvedEvent b) => recordIdResult != 0 ? recordIdResult : string.Compare(a.OwningLog, b.OwningLog, StringComparison.Ordinal); - private static int WithTieBreaker(int primaryResult, DisplayEventModel a, DisplayEventModel b) => + private static int WithTieBreaker(int primaryResult, ResolvedEvent a, ResolvedEvent b) => primaryResult != 0 ? primaryResult : FallbackTieBreaker(Nullable.Compare(a.RecordId, b.RecordId), a, b); - extension(DisplayEventModel? @event) + extension(ResolvedEvent? @event) { public bool Filter(IEnumerable filters) { @@ -279,7 +279,7 @@ public bool Filter(IEnumerable filters) return isEmpty || isFiltered; } - public DisplayEventModel? FilterByDate(FilterDateModel? dateFilter) + public ResolvedEvent? FilterByDate(FilterDateModel? dateFilter) { if (@event is null) { return null; } diff --git a/src/EventLogExpert.UI/Interfaces/IFilterService.cs b/src/EventLogExpert.UI/Interfaces/IFilterService.cs index fcb6b4af..baa8cd70 100644 --- a/src/EventLogExpert.UI/Interfaces/IFilterService.cs +++ b/src/EventLogExpert.UI/Interfaces/IFilterService.cs @@ -1,4 +1,4 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.Eventing.Common.Events; @@ -8,11 +8,11 @@ namespace EventLogExpert.UI.Interfaces; public interface IFilterService { - IReadOnlyDictionary> FilterActiveLogs( + IReadOnlyDictionary> FilterActiveLogs( IEnumerable logData, EventFilter eventFilter); - IReadOnlyList GetFilteredEvents(IEnumerable events, EventFilter eventFilter); + IReadOnlyList GetFilteredEvents(IEnumerable events, EventFilter eventFilter); bool TryParse(BasicFilter basicFilter, out string comparison); diff --git a/src/EventLogExpert.UI/Models/CompiledFilter.cs b/src/EventLogExpert.UI/Models/CompiledFilter.cs index b2d4a80c..a61b5aaf 100644 --- a/src/EventLogExpert.UI/Models/CompiledFilter.cs +++ b/src/EventLogExpert.UI/Models/CompiledFilter.cs @@ -9,6 +9,6 @@ namespace EventLogExpert.UI.Models; /// Immutable compiled artifact: the runnable predicate plus the cached XML-access flag used to decide whether /// logs must be opened with pre-rendered XML. ///
-/// The compiled lambda evaluated against each . -/// true when the source expression references . -public sealed record CompiledFilter(Func Predicate, bool RequiresXml); +/// The compiled lambda evaluated against each . +/// true when the source expression references . +public sealed record CompiledFilter(Func Predicate, bool RequiresXml); diff --git a/src/EventLogExpert.UI/Models/EventFilter.cs b/src/EventLogExpert.UI/Models/EventFilter.cs index 1dedb3d2..18534d5f 100644 --- a/src/EventLogExpert.UI/Models/EventFilter.cs +++ b/src/EventLogExpert.UI/Models/EventFilter.cs @@ -1,4 +1,4 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using System.Collections.Immutable; @@ -25,7 +25,7 @@ public EventFilter(FilterDateModel? dateFilter, ImmutableList filte /// Construction-time snapshot used by . public ImmutableArray Snapshots { get; } - /// True when any filter or sub-filter references . + /// True when any filter or sub-filter references . public bool RequiresXml { get; } private static bool ComputeRequiresXml(ImmutableList filters) diff --git a/src/EventLogExpert.UI/Models/EventLogData.cs b/src/EventLogExpert.UI/Models/EventLogData.cs index 21f7e60c..0b202a3e 100644 --- a/src/EventLogExpert.UI/Models/EventLogData.cs +++ b/src/EventLogExpert.UI/Models/EventLogData.cs @@ -1,4 +1,4 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.Eventing.Common.Channels; @@ -9,7 +9,7 @@ namespace EventLogExpert.UI.Models; public sealed record EventLogData( string Name, LogPathType Type, - IReadOnlyList Events) + IReadOnlyList Events) { public EventLogId Id { get; } = EventLogId.Create(); diff --git a/src/EventLogExpert.UI/Services/FilterCompiler.cs b/src/EventLogExpert.UI/Services/FilterCompiler.cs index c9855427..8856de93 100644 --- a/src/EventLogExpert.UI/Services/FilterCompiler.cs +++ b/src/EventLogExpert.UI/Services/FilterCompiler.cs @@ -45,7 +45,7 @@ public static bool TryCompile( try { var lambda = DynamicExpressionParser - .ParseLambda(s_parsingConfig, false, expression); + .ParseLambda(s_parsingConfig, false, expression); compiled = new CompiledFilter(lambda.Compile(), ContainsXmlMemberAccess(lambda)); @@ -74,8 +74,8 @@ private sealed class XmlMemberAccessVisitor : ExpressionVisitor protected override Expression VisitMember(MemberExpression node) { if (!Found && - node.Member.Name == nameof(DisplayEventModel.Xml) && - node.Member.DeclaringType == typeof(DisplayEventModel)) + node.Member.Name == nameof(ResolvedEvent.Xml) && + node.Member.DeclaringType == typeof(ResolvedEvent)) { Found = true; } diff --git a/src/EventLogExpert.UI/Services/FilterService.cs b/src/EventLogExpert.UI/Services/FilterService.cs index 745c0963..6609809b 100644 --- a/src/EventLogExpert.UI/Services/FilterService.cs +++ b/src/EventLogExpert.UI/Services/FilterService.cs @@ -15,7 +15,7 @@ public sealed class FilterService : IFilterService /// Outer parallelism only kicks in when the combined work justifies the scheduling overhead. private const int OuterParallelTotalEventThreshold = 10_000; - public IReadOnlyDictionary> FilterActiveLogs( + public IReadOnlyDictionary> FilterActiveLogs( IEnumerable logData, EventFilter eventFilter) { @@ -32,7 +32,7 @@ public IReadOnlyDictionary> FilterA // Multi-log heavy work: parallelize across logs, sequential within each log to avoid // oversubscribing the thread pool. - var results = new IReadOnlyList[logs.Count]; + var results = new IReadOnlyList[logs.Count]; try { @@ -49,7 +49,7 @@ public IReadOnlyDictionary> FilterA ExceptionDispatchInfo.Capture(aggregate.InnerExceptions[0]).Throw(); } - var filtered = new Dictionary>(logs.Count); + var filtered = new Dictionary>(logs.Count); for (var index = 0; index < logs.Count; index++) { @@ -60,17 +60,17 @@ public IReadOnlyDictionary> FilterA return filtered; } - public IReadOnlyList GetFilteredEvents( - IEnumerable events, + public IReadOnlyList GetFilteredEvents( + IEnumerable events, EventFilter eventFilter) { if (!FilterMethods.IsFilteringEnabled(eventFilter)) { - return events as IReadOnlyList ?? [.. events]; + return events as IReadOnlyList ?? [.. events]; } // PLINQ scheduling overhead exceeds the benefit below this threshold. - if (events is IReadOnlyCollection { Count: < 10_000 } collection) + if (events is IReadOnlyCollection { Count: < 10_000 } collection) { return FilterEventsSequential(collection, eventFilter); } @@ -118,7 +118,7 @@ public bool TryParseExpression(string? expression, out string error) try { - _ = Enumerable.Empty().AsQueryable() + _ = Enumerable.Empty().AsQueryable() .Where(ParsingConfig.Default, expression); return true; @@ -153,8 +153,8 @@ private static string EscapeStringLiteral(string? value) return builder.ToString(); } - private static IReadOnlyList FilterEventsSequential( - IEnumerable events, + private static IReadOnlyList FilterEventsSequential( + IEnumerable events, EventFilter eventFilter) => events .Where(e => e.FilterByDate(eventFilter.DateFilter) @@ -207,7 +207,7 @@ private static bool ShouldParallelizeAcrossLogs(IReadOnlyList logs foreach (var data in logs) { // If we can't cheaply size a log, assume the work is non-trivial and opt in to parallelism. - if (data.Events is not IReadOnlyCollection collection) + if (data.Events is not IReadOnlyCollection collection) { return true; } @@ -291,11 +291,11 @@ private static bool TryFormatData(FilterData data, string? joinPrefix, out strin return true; } - private Dictionary> BuildSequentialResult( + private Dictionary> BuildSequentialResult( IReadOnlyList logs, EventFilter eventFilter) { - var filtered = new Dictionary>(logs.Count); + var filtered = new Dictionary>(logs.Count); foreach (var data in logs) { diff --git a/src/EventLogExpert.UI/Store/EventLog/EventLogAction.cs b/src/EventLogExpert.UI/Store/EventLog/EventLogAction.cs index 47cb9349..4ff899f3 100644 --- a/src/EventLogExpert.UI/Store/EventLog/EventLogAction.cs +++ b/src/EventLogExpert.UI/Store/EventLog/EventLogAction.cs @@ -10,9 +10,9 @@ namespace EventLogExpert.UI.Store.EventLog; public sealed record EventLogAction { - public sealed record AddEvent(DisplayEventModel NewEvent); + public sealed record AddEvent(ResolvedEvent NewEvent); - public sealed record AddEventBuffered(IReadOnlyList UpdatedBuffer, bool IsFull); + public sealed record AddEventBuffered(IReadOnlyList UpdatedBuffer, bool IsFull); public sealed record AddEventSuccess(ImmutableDictionary ActiveLogs); @@ -20,20 +20,20 @@ public sealed record CloseAll; public sealed record CloseLog(EventLogId LogId, string LogName); - public sealed record LoadEvents(EventLogData LogData, IReadOnlyList Events); + public sealed record LoadEvents(EventLogData LogData, IReadOnlyList Events); - public sealed record LoadEventsPartial(EventLogData LogData, IReadOnlyList Events); + public sealed record LoadEventsPartial(EventLogData LogData, IReadOnlyList Events); public sealed record LoadNewEvents; public sealed record OpenLog(string LogName, LogPathType LogPathType, CancellationToken Token = default); public sealed record SelectEvent( - DisplayEventModel SelectedEvent, + ResolvedEvent SelectedEvent, bool IsMultiSelect = false, bool ShouldStaySelected = false); - public sealed record SelectEvents(IReadOnlyCollection SelectedEvents); + public sealed record SelectEvents(IReadOnlyCollection SelectedEvents); /// /// Replaces the entire selection with the supplied events, preserving input order @@ -47,8 +47,8 @@ public sealed record SelectEvents(IReadOnlyCollection Selecte /// The new focused event, or null to clear focus. Does not /// need to be a member of . public sealed record SetSelectedEvents( - IReadOnlyCollection SelectedEvents, - DisplayEventModel? SelectedEvent); + IReadOnlyCollection SelectedEvents, + ResolvedEvent? SelectedEvent); public sealed record SetContinuouslyUpdate(bool ContinuouslyUpdate); diff --git a/src/EventLogExpert.UI/Store/EventLog/EventLogEffects.cs b/src/EventLogExpert.UI/Store/EventLog/EventLogEffects.cs index ccf61a66..f2caf310 100644 --- a/src/EventLogExpert.UI/Store/EventLog/EventLogEffects.cs +++ b/src/EventLogExpert.UI/Store/EventLog/EventLogEffects.cs @@ -120,7 +120,7 @@ public Task HandleAddEvent(EventLogAction.AddEvent action, IDispatcher dispatche } else { - var updatedBuffer = new List(_eventLogState.Value.NewEventBuffer.Count + 1) + var updatedBuffer = new List(_eventLogState.Value.NewEventBuffer.Count + 1) { action.NewEvent }; @@ -230,7 +230,7 @@ public Task HandleLoadEvents(EventLogAction.LoadEvents action, IDispatcher dispa .Where(e => e.RecordId.HasValue && pending.SelectedIds.Contains(e.RecordId.Value)) .ToList(); - DisplayEventModel? selectedRestored = pending.SelectedId.HasValue + ResolvedEvent? selectedRestored = pending.SelectedId.HasValue ? action.Events.FirstOrDefault(e => e.RecordId == pending.SelectedId.Value) : null; @@ -496,7 +496,7 @@ public async Task HandleSetFilters(EventLogAction.SetFilters action, IDispatcher // once below so the new filter is applied to the post-mutation rows too. var snapshotById = activeLogsSnapshot.ToDictionary(d => d.Id); var currentByName = _eventLogState.Value.ActiveLogs; - var fresh = new Dictionary>(filteredActiveLogs.Count); + var fresh = new Dictionary>(filteredActiveLogs.Count); var staleLogs = new List(); foreach (var (logId, filteredEvents) in filteredActiveLogs) @@ -655,7 +655,7 @@ public void ReopenAfterDatabaseRemoval(IReadOnlyList snapshot) } /// Adds new events to the currently opened log - private static EventLogData AddEventsToOneLog(EventLogData logData, List eventsToAdd) + private static EventLogData AddEventsToOneLog(EventLogData logData, List eventsToAdd) { if (eventsToAdd.Count == 0) { return logData; } @@ -666,10 +666,10 @@ private static EventLogData AddEventsToOneLog(EventLogData logData, List DistributeEventsToManyLogs( ImmutableDictionary logsToUpdate, - IEnumerable eventsToDistribute) + IEnumerable eventsToDistribute) { // Group events by owning log once to avoid repeated enumeration - var eventsByLog = new Dictionary>(); + var eventsByLog = new Dictionary>(); foreach (var e in eventsToDistribute) { @@ -788,7 +788,7 @@ private async Task LoadLogAsync( FullMode = BoundedChannelFullMode.Wait }); - List events = []; + List events = []; await using var timer = new Timer( _ => @@ -799,7 +799,7 @@ private async Task LoadLogAsync( // is dispatched after ~3 seconds of loading. if (Interlocked.Increment(ref timerTick) <= 1) { return; } - List delta; + List delta; lock (events) { @@ -863,7 +863,7 @@ await Parallel.ForEachAsync( try { - List localBatch = new(batch.Length); + List localBatch = new(batch.Length); int localResolved = 0; foreach (var @event in batch) @@ -981,8 +981,8 @@ private void ProcessNewEventBuffer(EventLogState state, IDispatcher dispatcher) // Group the buffered events by owning log id, filter each group, and dispatch // a single batched append so the combined-view reducer only fires once. - var batched = new Dictionary>(); - var grouped = new Dictionary>(); + var batched = new Dictionary>(); + var grouped = new Dictionary>(); foreach (var bufferedEvent in state.NewEventBuffer) { diff --git a/src/EventLogExpert.UI/Store/EventLog/EventLogReducers.cs b/src/EventLogExpert.UI/Store/EventLog/EventLogReducers.cs index d6ca953a..fa9e7ed4 100644 --- a/src/EventLogExpert.UI/Store/EventLog/EventLogReducers.cs +++ b/src/EventLogExpert.UI/Store/EventLog/EventLogReducers.cs @@ -83,7 +83,7 @@ public static EventLogState ReduceLoadEventsPartial(EventLogState state, EventLo return state; } - var merged = new List(existingLog.Events.Count + action.Events.Count); + var merged = new List(existingLog.Events.Count + action.Events.Count); merged.AddRange(existingLog.Events); merged.AddRange(action.Events); @@ -114,7 +114,7 @@ public static EventLogState ReduceSelectEvent(EventLogState state, EventLogActio { // Reference equality keeps selection consistent with EventTable's // ReferenceEqualityComparer-based selection set. Value equality on - // DisplayEventModel (a record) would treat distinct-but-value-equal + // ResolvedEvent (a record) would treat distinct-but-value-equal // instances as the same — e.g., after a log reload, SelectedEvents // may still hold stale references that are value-equal to new ones. bool alreadySelected = ContainsReference(state.SelectedEvents, action.SelectedEvent); @@ -158,8 +158,8 @@ public static EventLogState ReduceSelectEvents(EventLogState state, EventLogActi // (for example, stale vs. fresh events after a reload) to coexist // so a stale reference in SelectedEvents isn't silently collapsed // with a freshly loaded copy. - var existing = new HashSet(state.SelectedEvents, ReferenceEqualityComparer.Instance); - List eventsToAdd = []; + var existing = new HashSet(state.SelectedEvents, ReferenceEqualityComparer.Instance); + List eventsToAdd = []; foreach (var selectedEvent in action.SelectedEvents) { @@ -179,7 +179,7 @@ public static EventLogState ReduceSelectEvents(EventLogState state, EventLogActi // same reference; fall back to a value-equal instance when only a // replacement reference is present. If neither is found, focus the // last incoming event so the restore path leaves something focused. - DisplayEventModel newSelectedEvent = eventsToAdd[^1]; + ResolvedEvent newSelectedEvent = eventsToAdd[^1]; if (state.SelectedEvent is null) { @@ -190,7 +190,7 @@ public static EventLogState ReduceSelectEvents(EventLogState state, EventLogActi }; } - DisplayEventModel? valueEqualMatch = null; + ResolvedEvent? valueEqualMatch = null; foreach (var selectedEvent in newSelection) { @@ -242,8 +242,8 @@ public static EventLogState ReduceSetSelectedEvents(EventLogState state, EventLo // Order-preserving distinct by reference identity. The caller (typically // EventTable) is responsible for ordering events according to the current // sort column; the reducer honors whatever order it receives. - var seen = new HashSet(ReferenceEqualityComparer.Instance); - var builder = ImmutableList.CreateBuilder(); + var seen = new HashSet(ReferenceEqualityComparer.Instance); + var builder = ImmutableList.CreateBuilder(); foreach (var selectedEvent in action.SelectedEvents) { @@ -274,7 +274,7 @@ public static EventLogState ReduceSetSelectedEvents(EventLogState state, EventLo }; } - private static bool ContainsReference(ImmutableList list, DisplayEventModel target) + private static bool ContainsReference(ImmutableList list, ResolvedEvent target) { foreach (var item in list) { @@ -285,11 +285,11 @@ private static bool ContainsReference(ImmutableList list, Dis } private static EventLogData GetEmptyLogData(string logName, LogPathType pathType) => - new(logName, pathType, new List().AsReadOnly()); + new(logName, pathType, new List().AsReadOnly()); - private static ImmutableList RemoveByReference( - ImmutableList list, - DisplayEventModel target) + private static ImmutableList RemoveByReference( + ImmutableList list, + ResolvedEvent target) { for (int i = 0; i < list.Count; i++) { @@ -303,8 +303,8 @@ private static ImmutableList RemoveByReference( } private static bool SelectionsEqualByReference( - ImmutableList left, - ImmutableList right) + ImmutableList left, + ImmutableList right) { if (ReferenceEquals(left, right)) { return true; } @@ -321,7 +321,7 @@ private static bool SelectionsEqualByReference( private static EventLogState UpdateActiveLog( EventLogState state, EventLogData logData, - IReadOnlyList events) => + IReadOnlyList events) => state with { ActiveLogs = state.ActiveLogs diff --git a/src/EventLogExpert.UI/Store/EventLog/EventLogState.cs b/src/EventLogExpert.UI/Store/EventLog/EventLogState.cs index f463c21d..bae20ee5 100644 --- a/src/EventLogExpert.UI/Store/EventLog/EventLogState.cs +++ b/src/EventLogExpert.UI/Store/EventLog/EventLogState.cs @@ -21,11 +21,11 @@ public sealed record EventLogState public bool ContinuouslyUpdate { get; init; } = false; - public IReadOnlyList NewEventBuffer { get; init; } = []; + public IReadOnlyList NewEventBuffer { get; init; } = []; public bool NewEventBufferIsFull { get; init; } - public DisplayEventModel? SelectedEvent { get; init; } + public ResolvedEvent? SelectedEvent { get; init; } - public ImmutableList SelectedEvents { get; init; } = []; + public ImmutableList SelectedEvents { get; init; } = []; } diff --git a/src/EventLogExpert.UI/Store/EventTable/EventTableAction.cs b/src/EventLogExpert.UI/Store/EventTable/EventTableAction.cs index b1d8feb3..c5ff8fe9 100644 --- a/src/EventLogExpert.UI/Store/EventTable/EventTableAction.cs +++ b/src/EventLogExpert.UI/Store/EventTable/EventTableAction.cs @@ -1,4 +1,4 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.Eventing.Common.Events; @@ -11,9 +11,9 @@ public sealed record EventTableAction { public sealed record AddTable(EventLogData LogData); - public sealed record AppendTableEvents(EventLogId LogId, IReadOnlyList Events); + public sealed record AppendTableEvents(EventLogId LogId, IReadOnlyList Events); - public sealed record AppendTableEventsBatch(IReadOnlyDictionary> EventsByLog); + public sealed record AppendTableEventsBatch(IReadOnlyDictionary> EventsByLog); public sealed record CloseAll; @@ -43,7 +43,7 @@ public sealed record ToggleLoading(EventLogId LogId); public sealed record ToggleSorting; public sealed record UpdateDisplayedEvents( - IReadOnlyDictionary> ActiveLogs); + IReadOnlyDictionary> ActiveLogs); - public sealed record UpdateTable(EventLogId LogId, IReadOnlyList Events); + public sealed record UpdateTable(EventLogId LogId, IReadOnlyList Events); } diff --git a/src/EventLogExpert.UI/Store/EventTable/EventTableReducers.cs b/src/EventLogExpert.UI/Store/EventTable/EventTableReducers.cs index dca12c39..468e8f95 100644 --- a/src/EventLogExpert.UI/Store/EventTable/EventTableReducers.cs +++ b/src/EventLogExpert.UI/Store/EventTable/EventTableReducers.cs @@ -92,7 +92,7 @@ public static EventTableState ReduceAppendTableEventsBatch( // Skip batches for closed logs: avoid resurrecting events and stale counts. int totalNew = 0; - var combinedBatch = new List(); + var combinedBatch = new List(); var counts = state.EventCountByLog; var updatedTables = state.EventTables; @@ -313,7 +313,7 @@ public static EventTableState ReduceUpdateDisplayedEvents( if (tablesById.ContainsKey(logId)) { totalCount += events.Count; } } - var concatenated = new List(totalCount); + var concatenated = new List(totalCount); for (int eventIndex = 0; eventIndex < state.DisplayedEvents.Count; eventIndex++) { @@ -386,11 +386,11 @@ public static EventTableState ReduceUpdateTable(EventTableState state, EventTabl }; } - private static IReadOnlyList FilterByOwningLog( - IReadOnlyList events, + private static IReadOnlyList FilterByOwningLog( + IReadOnlyList events, string owningLog) { - var filtered = new List(events.Count); + var filtered = new List(events.Count); for (int eventIndex = 0; eventIndex < events.Count; eventIndex++) { @@ -405,11 +405,11 @@ private static IReadOnlyList FilterByOwningLog( return filtered.AsReadOnly(); } - private static IReadOnlyList FilterOutOwningLog( - IReadOnlyList events, + private static IReadOnlyList FilterOutOwningLog( + IReadOnlyList events, string owningLog) { - var filtered = new List(events.Count); + var filtered = new List(events.Count); for (int eventIndex = 0; eventIndex < events.Count; eventIndex++) { @@ -436,7 +436,7 @@ private static ImmutableDictionary IncrementCount( return counts.SetItem(logId, current + delta); } - private static EventTableModel SetComputerNameIfFirstEvent(EventTableModel table, IReadOnlyList newEvents) + private static EventTableModel SetComputerNameIfFirstEvent(EventTableModel table, IReadOnlyList newEvents) { if (!string.IsNullOrEmpty(table.ComputerName) || newEvents.Count == 0) { return table; } diff --git a/src/EventLogExpert.UI/Store/EventTable/EventTableState.cs b/src/EventLogExpert.UI/Store/EventTable/EventTableState.cs index faeb5984..0e6408a3 100644 --- a/src/EventLogExpert.UI/Store/EventTable/EventTableState.cs +++ b/src/EventLogExpert.UI/Store/EventTable/EventTableState.cs @@ -13,7 +13,7 @@ public sealed record EventTableState { public ImmutableList EventTables { get; init; } = []; - public IReadOnlyList DisplayedEvents { get; init; } = []; + public IReadOnlyList DisplayedEvents { get; init; } = []; public ImmutableDictionary EventCountByLog { get; init; } = ImmutableDictionary.Empty; diff --git a/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs b/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs index a9a160e9..25041eb3 100644 --- a/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs +++ b/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs @@ -25,7 +25,7 @@ public sealed partial class DetailsPane /// (show "Resolving XML..."); empty string means resolved-with-no-content (e.g. live-watcher event /// without a record id, or render failure); any other value is the rendered XML. private string? _resolvedXml; - private DisplayEventModel? _selectedEvent; + private ResolvedEvent? _selectedEvent; /// Cancels any in-flight XML resolution when the selection changes again before completion. private CancellationTokenSource? _xmlResolveCts; @@ -37,7 +37,7 @@ public sealed partial class DetailsPane [Inject] private IPreferencesProvider PreferencesProvider { get; init; } = null!; - [Inject] private IStateSelection SelectedEvent { get; init; } = null!; + [Inject] private IStateSelection SelectedEvent { get; init; } = null!; [Inject] private ISettingsService Settings { get; init; } = null!; @@ -142,7 +142,7 @@ private void HandleKeyDownXml(KeyboardEventArgs e) } } - private async void OnSelectedEventChanged(object? sender, DisplayEventModel? selectedEvent) + private async void OnSelectedEventChanged(object? sender, ResolvedEvent? selectedEvent) { try { diff --git a/src/EventLogExpert/Components/Sections/EventTable.razor b/src/EventLogExpert/Components/Sections/EventTable.razor index 4d134d00..3a30e47c 100644 --- a/src/EventLogExpert/Components/Sections/EventTable.razor +++ b/src/EventLogExpert/Components/Sections/EventTable.razor @@ -1,4 +1,4 @@ -@using EventLogExpert.Eventing.Common.Events +@using EventLogExpert.Eventing.Common.Events @inherits FluxorComponent @@ -47,7 +47,7 @@ @if (_currentTable is not null) { - + s_warnedUnknownColors = []; - private readonly Dictionary _highlightCache = new(ReferenceEqualityComparer.Instance); + private readonly Dictionary _highlightCache = new(ReferenceEqualityComparer.Instance); - private IReadOnlyList _activeDisplayedEvents = []; - private IReadOnlyList? _cachedFilteredCanonical; + private IReadOnlyList _activeDisplayedEvents = []; + private IReadOnlyList? _cachedFilteredCanonical; private EventLogId? _cachedFilteredTableId; - private IReadOnlyList? _cachedFilteredView; + private IReadOnlyList? _cachedFilteredView; private EventTableModel? _currentTable; private DotNetObjectReference? _dotNetRef; private ColumnName[] _enabledColumns = null!; @@ -47,24 +47,24 @@ public sealed partial class EventTable private ImmutableList _filters = []; private bool _focusActiveOnNextRender; private string _headerName = string.Empty; - private IReadOnlyList? _lastIndexedDisplayedEvents; + private IReadOnlyList? _lastIndexedDisplayedEvents; // View-local cursor: the row that is the moving end of a range selection within the // current table. May briefly diverge from _selectedEvent during local keyboard nav // (advanced before the dispatch round-trip) and after RebuildRowIndexMap rebinds it // to the equivalent row in a freshly built DisplayedEvents list. Defaults to the // same row as _selectionAnchor for single-row selections. - private DisplayEventModel? _localCursor; + private ResolvedEvent? _localCursor; private int _pageSize = DefaultPageSize; private ColumnName[] _previousEnabledColumns = []; private bool _resortSelectionOnNextRender; - private Dictionary _rowIndexMap = new(ReferenceEqualityComparer.Instance); - private DisplayEventModel? _selectedEvent; - private ImmutableList _selectedEvents = []; - private HashSet _selectedSet = new(ReferenceEqualityComparer.Instance); + private Dictionary _rowIndexMap = new(ReferenceEqualityComparer.Instance); + private ResolvedEvent? _selectedEvent; + private ImmutableList _selectedEvents = []; + private HashSet _selectedSet = new(ReferenceEqualityComparer.Instance); // The fixed end of a range selection — set on plain click, Ctrl+Click, // and any keyboard nav that establishes a single selection. Reused for // Shift+Click and Shift+Arrow to compute the range. - private DisplayEventModel? _selectionAnchor; + private ResolvedEvent? _selectionAnchor; private TimeZoneInfo _timeZoneSettings = null!; [Inject] private IClipboardService ClipboardService { get; init; } = null!; @@ -81,9 +81,9 @@ public sealed partial class EventTable [Inject] private IMenuService MenuService { get; init; } = null!; - [Inject] private IStateSelection SelectedEvent { get; init; } = null!; + [Inject] private IStateSelection SelectedEvent { get; init; } = null!; - [Inject] private IStateSelection> SelectedEvents { get; init; } = null!; + [Inject] private IStateSelection> SelectedEvents { get; init; } = null!; [Inject] private ISettingsService Settings { get; init; } = null!; @@ -190,7 +190,7 @@ protected override async Task OnInitializedAsync() _selectedEvent = SelectedEvent.Value; _localCursor = _selectedEvent; _selectedEvents = SelectedEvents.Value; - _selectedSet = new HashSet(_selectedEvents, ReferenceEqualityComparer.Instance); + _selectedSet = new HashSet(_selectedEvents, ReferenceEqualityComparer.Instance); _filters = FilterPaneState.Value.Filters; _timeZoneSettings = Settings.TimeZoneInfo; @@ -225,13 +225,13 @@ protected override bool ShouldRender() if (selectionChanged) { _selectedEvents = SelectedEvents.Value; - // Reference equality is intentional. Even though DisplayEventModel is + // Reference equality is intentional. Even though ResolvedEvent is // now a fully immutable record (no mutating ResolveXml() workaround), // value-equality requires hashing every string field on every selection // mutation. Reference equality keeps selection bookkeeping O(1) and // also avoids any chance that two distinct event instances that happen // to be value-equal would collapse into a single selected row. - _selectedSet = new HashSet(_selectedEvents, ReferenceEqualityComparer.Instance); + _selectedSet = new HashSet(_selectedEvents, ReferenceEqualityComparer.Instance); } if (selectedEventChanged) @@ -269,9 +269,9 @@ private static string GetLevelClass(string level) => private static string GetLogShortName(string owningLog) => owningLog[(owningLog.LastIndexOf('\\') + 1)..]; - private static DisplayEventModel? ResolveByKey( - IReadOnlyList displayedEvents, - DisplayEventModel? candidate) + private static ResolvedEvent? ResolveByKey( + IReadOnlyList displayedEvents, + ResolvedEvent? candidate) { if (candidate is null) { return null; } @@ -295,7 +295,7 @@ private static string GetLogShortName(string owningLog) => return null; } - private void ApplySelectedFilter(DisplayEventModel selectedEvent, FilterCategory category, bool exclude) + private void ApplySelectedFilter(ResolvedEvent selectedEvent, FilterCategory category, bool exclude) { string filterValue = category switch { @@ -331,10 +331,10 @@ private void ApplySelectedFilter(DisplayEventModel selectedEvent, FilterCategory Dispatcher.Dispatch(new FilterPaneAction.SetFilter(filter)); } - private IReadOnlyList BuildRange( - IReadOnlyList displayedEvents, - DisplayEventModel anchor, - DisplayEventModel selected) + private IReadOnlyList BuildRange( + IReadOnlyList displayedEvents, + ResolvedEvent anchor, + ResolvedEvent selected) { // O(1) lookups via _rowIndexMap; fall back to a linear scan only if // the map is stale (e.g., during a render between table rebuilds). @@ -358,7 +358,7 @@ private IReadOnlyList BuildRange( int start = Math.Min(anchorIndex, activeIndex); int end = Math.Max(anchorIndex, activeIndex); - var range = new DisplayEventModel[end - start + 1]; + var range = new ResolvedEvent[end - start + 1]; for (int i = 0; i < range.Length; i++) { @@ -368,15 +368,15 @@ private IReadOnlyList BuildRange( return range; } - private void DispatchSetSelection(IReadOnlyList events, DisplayEventModel? selected) + private void DispatchSetSelection(IReadOnlyList events, ResolvedEvent? selected) { // Sort the selection by current row-index for events in this table; events // belonging to other open logs (not in _rowIndexMap) preserve their existing // relative order at the tail. De-dupe by reference identity throughout so // SetSelectedEvents never has to re-process duplicates. - var seen = new HashSet(ReferenceEqualityComparer.Instance); - List<(DisplayEventModel Event, int Index)> inTable = new(events.Count); - List outOfTable = []; + var seen = new HashSet(ReferenceEqualityComparer.Instance); + List<(ResolvedEvent Event, int Index)> inTable = new(events.Count); + List outOfTable = []; foreach (var selectedEvent in events) { @@ -394,7 +394,7 @@ private void DispatchSetSelection(IReadOnlyList events, Displ inTable.Sort(static (left, right) => left.Index.CompareTo(right.Index)); - var ordered = new List(inTable.Count + outOfTable.Count); + var ordered = new List(inTable.Count + outOfTable.Count); foreach (var entry in inTable) { ordered.Add(entry.Event); } @@ -420,7 +420,7 @@ private async Task FocusActiveRow() } } - private int GetActiveIndex(IReadOnlyList displayedEvents) + private int GetActiveIndex(IReadOnlyList displayedEvents) { if (_localCursor is not null && _rowIndexMap.TryGetValue(_localCursor, out int idx)) { @@ -442,7 +442,7 @@ private int GetActiveIndex(IReadOnlyList displayedEvents) private int GetColumnWidth(ColumnName column) => _eventTableState.ColumnWidths.TryGetValue(column, out int width) ? width : ColumnDefaults.GetWidth(column); - private string GetCss(DisplayEventModel @event) => + private string GetCss(ResolvedEvent @event) => _selectedSet.Contains(@event) ? "table-row selected" : "table-row"; private string GetDateColumnHeader() => @@ -450,7 +450,7 @@ private string GetDateColumnHeader() => "Date and Time" : $"Date and Time {Settings.TimeZoneInfo.DisplayName.Split(" ").First()}"; - private string? GetHighlight(DisplayEventModel @event) + private string? GetHighlight(ResolvedEvent @event) { // Selected rows show selection styling (.selected wins via !important); // skip cache writes so deselecting doesn't require a refill. @@ -503,7 +503,7 @@ private ColumnName[] GetOrderedEnabledColumns() .ToArray(); } - private int GetRowIndex(DisplayEventModel evt) => + private int GetRowIndex(ResolvedEvent evt) => _rowIndexMap.TryGetValue(evt, out int index) ? index + 2 : 2; private async Task HandleKeyDown(KeyboardEventArgs args) @@ -620,7 +620,7 @@ private void InvokeContextMenu(MouseEventArgs args) private void InvokeTableColumnMenu(MouseEventArgs args) => MenuService.OpenAt(args.ClientX, args.ClientY, ShowColumnMenuItems()); - private bool IsSelectionOutOfSortOrder(IReadOnlyList selection) + private bool IsSelectionOutOfSortOrder(IReadOnlyList selection) { int lastIndex = -1; @@ -657,7 +657,7 @@ private void RebuildRowIndexMap() _lastIndexedDisplayedEvents = displayedEvents; _rowIndexMap = new(displayedEvents.Count, ReferenceEqualityComparer.Instance); - // New event-list reference means stored DisplayEventModel instances + // New event-list reference means stored ResolvedEvent instances // are stale; clearing prevents memory growth across log reloads. _highlightCache.Clear(); @@ -703,7 +703,7 @@ private async void RescrollToSelected() } } - private IReadOnlyList ResolveActiveDisplayedEvents() + private IReadOnlyList ResolveActiveDisplayedEvents() { if (_currentTable is null) { return []; } @@ -722,7 +722,7 @@ private IReadOnlyList ResolveActiveDisplayedEvents() ? trackedCount : 0; - var filtered = new List(expectedCapacity); + var filtered = new List(expectedCapacity); for (int eventIndex = 0; eventIndex < canonical.Count; eventIndex++) { @@ -787,7 +787,7 @@ private async Task ScrollToSelectedEvent() } } - private void SelectEvent(MouseEventArgs args, DisplayEventModel @event) + private void SelectEvent(MouseEventArgs args, ResolvedEvent @event) { var displayedEvents = _activeDisplayedEvents; @@ -814,7 +814,7 @@ private void SelectEvent(MouseEventArgs args, DisplayEventModel @event) // Ctrl+Shift+Click: additive range. Merge existing // selection with the new range. Dedupe by reference is // handled centrally inside DispatchSetSelection. - var merged = new List(_selectedEvents.Count + range.Count); + var merged = new List(_selectedEvents.Count + range.Count); merged.AddRange(_selectedEvents); merged.AddRange(range); DispatchSetSelection(merged, @event); @@ -835,7 +835,7 @@ private void SelectEvent(MouseEventArgs args, DisplayEventModel @event) if (_selectedSet.Contains(@event)) { - var remaining = new List(_selectedEvents.Count); + var remaining = new List(_selectedEvents.Count); foreach (var existingEvent in _selectedEvents) { @@ -846,7 +846,7 @@ private void SelectEvent(MouseEventArgs args, DisplayEventModel @event) } else { - var combined = new List(_selectedEvents.Count + 1); + var combined = new List(_selectedEvents.Count + 1); combined.AddRange(_selectedEvents); combined.Add(@event); DispatchSetSelection(combined, @event); @@ -912,7 +912,7 @@ private IReadOnlyList ShowColumnMenuItems() return items; } - private IReadOnlyList ShowContextMenuItems(DisplayEventModel selectedEvent) + private IReadOnlyList ShowContextMenuItems(ResolvedEvent selectedEvent) { return [ @@ -933,7 +933,7 @@ private IReadOnlyList ShowContextMenuItems(DisplayEventModel selectedE ]; } - private IReadOnlyList ShowFilterCategoryItems(DisplayEventModel selectedEvent, bool exclude) + private IReadOnlyList ShowFilterCategoryItems(ResolvedEvent selectedEvent, bool exclude) { var items = new List(); diff --git a/src/EventLogExpert/Components/Sections/SplitLogTabPane.razor.cs b/src/EventLogExpert/Components/Sections/SplitLogTabPane.razor.cs index 7dfa05ad..56ba94ff 100644 --- a/src/EventLogExpert/Components/Sections/SplitLogTabPane.razor.cs +++ b/src/EventLogExpert/Components/Sections/SplitLogTabPane.razor.cs @@ -55,9 +55,12 @@ private static string GetTabTooltip(EventTableModel table) { if (table.IsCombined) { return string.Empty; } - return $"{(table.LogPathType == LogPathType.File ? "Log File: " : "Live Log: ")} {table.FileName}\n" + - $"Log Name: {table.LogName}\n" + - $"Computer Name: {table.ComputerName}"; + return table.LogPathType == LogPathType.File + ? $"Log File: {table.FileName}\n" + + $"Log Name: {table.LogName}\n" + + $"Computer Name: {table.ComputerName}" + : $"Live Log: {table.LogName}\n" + + $"Computer Name: {table.ComputerName}"; } private void CloseLog(EventTableModel table) => diff --git a/src/EventLogExpert/Services/ClipboardService.cs b/src/EventLogExpert/Services/ClipboardService.cs index 10b5dfa8..fae0e770 100644 --- a/src/EventLogExpert/Services/ClipboardService.cs +++ b/src/EventLogExpert/Services/ClipboardService.cs @@ -18,16 +18,16 @@ namespace EventLogExpert.Services; public sealed class ClipboardService : IClipboardService { private readonly IStateSelection> _eventTableColumns; - private readonly IStateSelection _selectedEvent; - private readonly IStateSelection> _selectedEvents; + private readonly IStateSelection _selectedEvent; + private readonly IStateSelection> _selectedEvents; private readonly ISettingsService _settings; private readonly ITraceLogger _traceLogger; private readonly IEventXmlResolver _xmlResolver; public ClipboardService( IStateSelection> eventTableColumns, - IStateSelection> selectedEvents, - IStateSelection selectedEvent, + IStateSelection> selectedEvents, + IStateSelection selectedEvent, ISettingsService settings, IEventXmlResolver xmlResolver, ITraceLogger traceLogger) @@ -93,7 +93,7 @@ private static string GetLogShortName(string owningLog) => private void AppendFormattedEvent( StringBuilder builder, CopyType copyType, - DisplayEventModel @event, + ResolvedEvent @event, string xml) { switch (copyType) @@ -179,7 +179,7 @@ private void AppendFormattedEvent( } } - private string FormatEventForCopy(CopyType copyType, DisplayEventModel @event, string xml) + private string FormatEventForCopy(CopyType copyType, ResolvedEvent @event, string xml) { if (copyType == CopyType.Xml) { @@ -266,7 +266,7 @@ private async Task GetFormattedEvent(CopyType? copyType) return stringToCopy.ToString(); } - private async Task ResolveXmlAsync(DisplayEventModel evt, SemaphoreSlim resolverLock) + private async Task ResolveXmlAsync(ResolvedEvent evt, SemaphoreSlim resolverLock) { await resolverLock.WaitAsync().ConfigureAwait(false); diff --git a/src/EventLogExpert/Services/MauiMenuActionService.cs b/src/EventLogExpert/Services/MauiMenuActionService.cs index f57518d1..8662b271 100644 --- a/src/EventLogExpert/Services/MauiMenuActionService.cs +++ b/src/EventLogExpert/Services/MauiMenuActionService.cs @@ -146,17 +146,12 @@ public async Task OpenFileAsync(bool combineLog) if (!files.Any()) { return; } - if (!combineLog) - { - await CloseAllLogsAsync(); - } - var paths = files .Where(file => file is not null && !string.IsNullOrEmpty(file.FullPath)) .Select(file => (file!.FullPath, LogPathType.File)) .ToList(); - await OpenLogsBatchAsync(paths, combineLog: true); + await OpenLogsBatchAsync(paths, combineLog); } public async Task OpenFolderAsync(bool combineLog) @@ -193,12 +188,7 @@ public async Task OpenFolderAsync(bool combineLog) if (files.Count == 0) { return; } - if (!combineLog) - { - await CloseAllLogsAsync(); - } - - await OpenLogsBatchAsync(files, combineLog: true); + await OpenLogsBatchAsync(files, combineLog); } public Task OpenIssueAsync() => OpenBrowserAsync("https://github.com/microsoft/EventLogExpert/issues/new"); @@ -209,7 +199,7 @@ public Task OpenLiveLogAsync(string logName, bool combineLog) => public async Task OpenLogAsync(string logPath, LogPathType pathType, bool combineLog = false) { if (string.IsNullOrWhiteSpace(logPath) || - (combineLog && _eventLogState.Value.ActiveLogs.ContainsKey(logPath))) { return OpenLogStatus.Loaded; } + (combineLog && _eventLogState.Value.ActiveLogs.ContainsKey(logPath))) { return OpenLogStatus.Skipped; } EventLogInformation? eventLogInformation; @@ -224,13 +214,13 @@ await _dialogService.ShowAlert( "Please relaunch with \"Run as Administrator\" to open this log", "Ok"); - return OpenLogStatus.Loaded; + return OpenLogStatus.Failed; } catch (Exception ex) { await _dialogService.ShowAlert("Failed to open Log", $"Exception: {ex.Message}", "Ok"); - return OpenLogStatus.Loaded; + return OpenLogStatus.Failed; } if (eventLogInformation.RecordCount is null or <= 0) @@ -250,26 +240,35 @@ await _dialogService.ShowAlert( } _dispatcher.Dispatch(new EventLogAction.OpenLog(logPath, pathType, _cancellationTokenSource.Token)); - return OpenLogStatus.Loaded; + return OpenLogStatus.Opened; } /// /// Opens each log in sequentially and surfaces a single banner alert at the end naming - /// every log that contained zero events. Use this from any call site that may open multiple logs in one user - /// gesture (multi-file picker, folder open, drag-drop, command line) so the user sees one batched alert instead - /// of one popup per empty file. + /// every log that contained zero events. controls whether the first successful + /// open closes existing logs first; subsequent opens within the batch always coalesce with the new state. Use + /// this from any call site that may open multiple logs in one user gesture (multi-file picker, folder open, + /// drag-drop, command line) so the user sees one batched alert instead of one popup per empty file. /// public async Task OpenLogsBatchAsync(IEnumerable<(string Path, LogPathType Type)> logs, bool combineLog) { ArgumentNullException.ThrowIfNull(logs); List? emptyDisplayNames = null; + var combineForCall = combineLog; foreach (var (path, type) in logs) { - if (await OpenLogAsync(path, type, combineLog) == OpenLogStatus.Empty) + // Only Opened consumed the close-existing semantics; Skipped/Failed/Empty did not, + // so combineForCall must NOT flip until a real open happens. + switch (await OpenLogAsync(path, type, combineForCall)) { - (emptyDisplayNames ??= []).Add(GetEmptyLogDisplayName(path, type)); + case OpenLogStatus.Opened: + combineForCall = true; + break; + case OpenLogStatus.Empty: + (emptyDisplayNames ??= []).Add(GetEmptyLogDisplayName(path, type)); + break; } } diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventXmlResolverTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventXmlResolverTests.cs index 5e2799eb..4720474c 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventXmlResolverTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventXmlResolverTests.cs @@ -194,17 +194,17 @@ private static EventXmlResolver CreateBoundedTrackingResolver( getResolveCallCount = () => Volatile.Read(ref resolveCount); return new EventXmlResolver( - (owningLog, recordId, LogPathType) => + (owningLog, recordId, logPathType) => { Interlocked.Increment(ref resolveCount); - return resolve(new ResolveKey(owningLog, recordId, LogPathType)); + return resolve(new ResolveKey(owningLog, recordId, logPathType)); }, initialCapacity, maxCapacity); } - private static DisplayEventModel CreateEvent(long? recordId, string owningLog = "TestLog") => + private static ResolvedEvent CreateEvent(long? recordId, string owningLog = "TestLog") => new(owningLog, LogPathType.Channel) { RecordId = recordId, @@ -220,11 +220,11 @@ private static EventXmlResolver CreateTrackingResolver( getResolveCallCount = () => Volatile.Read(ref resolveCount); return new EventXmlResolver( - (owningLog, recordId, LogPathType) => + (owningLog, recordId, logPathType) => { Interlocked.Increment(ref resolveCount); - return resolve(new ResolveKey(owningLog, recordId, LogPathType)); + return resolve(new ResolveKey(owningLog, recordId, logPathType)); }); } diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/VersatileEventResolverTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/VersatileEventResolverTests.cs index e3c3373a..24a1b4bb 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/VersatileEventResolverTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/VersatileEventResolverTests.cs @@ -308,7 +308,7 @@ public void ResolveEvent_WithDatabaseResolver_ShouldResolveEvent() }; // Act - DisplayEventModel displayEvent; + ResolvedEvent displayEvent; using (var resolver = new EventResolver(dbCollection)) { displayEvent = resolver.ResolveEvent(eventRecord); @@ -380,8 +380,8 @@ public void ResolveEvent_WithMixedDbAndUnknownProviders_ShouldOnlyApplyDatabaseT var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath)); - DisplayEventModel dbDisplayEvent; - DisplayEventModel unknownDisplayEvent; + ResolvedEvent dbDisplayEvent; + ResolvedEvent unknownDisplayEvent; using (var resolver = new EventResolver(dbCollection)) { @@ -611,8 +611,8 @@ public void SwitchBetweenResolvers_WithDifferentInstances_ShouldWorkIndependentl var eventRecord = EventUtils.CreateBasicEvent(); // Act - DisplayEventModel localEvent; - DisplayEventModel databaseEvent; + ResolvedEvent localEvent; + ResolvedEvent databaseEvent; using (var localResolver = new EventResolver()) { diff --git a/tests/Unit/EventLogExpert.UI.Tests/DateRangeDefaultsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/DateRangeDefaultsTests.cs index 3f95cd46..cfddc172 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/DateRangeDefaultsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/DateRangeDefaultsTests.cs @@ -129,7 +129,7 @@ public void ComputeFromActiveLogs_WhenSingleLog_ReturnsItsBoundsRounded() private static EventLogData CreateLog(string name, DateTime newest, DateTime oldest) { // Events are stored newest-first (sorted by RecordId descending in production). - var events = new List + var events = new List { EventUtils.CreateTestEvent(timeCreated: newest), EventUtils.CreateTestEvent(timeCreated: oldest) diff --git a/tests/Unit/EventLogExpert.UI.Tests/FilterMethodsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/FilterMethodsTests.cs index 4b549baa..976f5f6b 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/FilterMethodsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/FilterMethodsTests.cs @@ -187,7 +187,7 @@ public void FilterByDate_WhenEventExactlyAtBefore_ShouldReturnEvent() public void FilterByDate_WhenEventIsNull_ShouldReturnNull() { // Arrange - DisplayEventModel? @event = null; + ResolvedEvent? @event = null; var dateFilter = new FilterDateModel { @@ -226,7 +226,7 @@ public void FilterByDate_WhenEventWithinRange_ShouldReturnEvent() public void Filter_WhenEventIsNull_ShouldReturnFalse() { // Arrange - DisplayEventModel? @event = null; + ResolvedEvent? @event = null; var filters = new List { CreateFilter(Constants.FilterIdEquals100) }; // Act @@ -749,7 +749,7 @@ public void IsFilteringEnabled_WhenNoDateFilterAndNoFilters_ShouldReturnFalse() public void SortEvents_WhenDescendingTrue_ShouldSortInDescendingOrder() { // Arrange - var events = new List + var events = new List { EventUtils.CreateTestEvent(100), EventUtils.CreateTestEvent(300), @@ -769,7 +769,7 @@ public void SortEvents_WhenDescendingTrue_ShouldSortInDescendingOrder() public void SortEvents_WhenEmptyCollection_ShouldReturnEmptyList() { // Arrange - var events = new List(); + var events = new List(); // Act var result = events.SortEvents(ColumnName.EventId); @@ -782,7 +782,7 @@ public void SortEvents_WhenEmptyCollection_ShouldReturnEmptyList() public void SortEvents_WhenNoOrderSpecifiedDescending_ShouldSortByRecordIdDescending() { // Arrange - var events = new List + var events = new List { EventUtils.CreateTestEvent(recordId: 1), EventUtils.CreateTestEvent(recordId: 3), @@ -802,7 +802,7 @@ public void SortEvents_WhenNoOrderSpecifiedDescending_ShouldSortByRecordIdDescen public void SortEvents_WhenNoOrderSpecified_ShouldSortByRecordIdAscending() { // Arrange - var events = new List + var events = new List { EventUtils.CreateTestEvent(recordId: 3), EventUtils.CreateTestEvent(recordId: 1), @@ -826,7 +826,7 @@ public void SortEvents_WhenOrderByActivityId_ShouldSortByActivityId() var guid2 = Guid.Parse("00000000-0000-0000-0000-000000000002"); var guid3 = Guid.Parse("00000000-0000-0000-0000-000000000003"); - var events = new List + var events = new List { EventUtils.CreateTestEvent(activityId: guid3), EventUtils.CreateTestEvent(activityId: guid1), @@ -846,7 +846,7 @@ public void SortEvents_WhenOrderByActivityId_ShouldSortByActivityId() public void SortEvents_WhenOrderByComputerName_ShouldSortByComputerName() { // Arrange - var events = new List + var events = new List { EventUtils.CreateTestEvent(computerName: "Server03"), EventUtils.CreateTestEvent(computerName: "Server01"), @@ -868,7 +868,7 @@ public void SortEvents_WhenOrderByDateAndTime_ShouldSortByTimeCreated() // Arrange var baseTime = DateTime.Now; - var events = new List + var events = new List { EventUtils.CreateTestEvent(timeCreated: baseTime.AddHours(2)), EventUtils.CreateTestEvent(timeCreated: baseTime), @@ -888,7 +888,7 @@ public void SortEvents_WhenOrderByDateAndTime_ShouldSortByTimeCreated() public void SortEvents_WhenOrderByEventId_ShouldSortById() { // Arrange - var events = new List + var events = new List { EventUtils.CreateTestEvent(300), EventUtils.CreateTestEvent(100), @@ -908,7 +908,7 @@ public void SortEvents_WhenOrderByEventId_ShouldSortById() public void SortEvents_WhenOrderByKeywords_ShouldSortByKeywordsDisplayName() { // Arrange - var events = new List + var events = new List { EventUtils.CreateTestEvent(keywords: ["Zebra"]), EventUtils.CreateTestEvent(keywords: ["Apple"]), @@ -928,7 +928,7 @@ public void SortEvents_WhenOrderByKeywords_ShouldSortByKeywordsDisplayName() public void SortEvents_WhenOrderByLevel_ShouldSortByLevel() { // Arrange - var events = new List + var events = new List { EventUtils.CreateTestEvent(level: "Warning"), EventUtils.CreateTestEvent(level: "Error"), @@ -948,7 +948,7 @@ public void SortEvents_WhenOrderByLevel_ShouldSortByLevel() public void SortEvents_WhenOrderByLog_ShouldSortByLogName() { // Arrange - var events = new List + var events = new List { EventUtils.CreateTestEvent(logName: "System"), EventUtils.CreateTestEvent(logName: "Application"), @@ -968,7 +968,7 @@ public void SortEvents_WhenOrderByLog_ShouldSortByLogName() public void SortEvents_WhenOrderByProcessId_ShouldSortByProcessId() { // Arrange - var events = new List + var events = new List { EventUtils.CreateTestEvent(processId: 300), EventUtils.CreateTestEvent(processId: 100), @@ -988,7 +988,7 @@ public void SortEvents_WhenOrderByProcessId_ShouldSortByProcessId() public void SortEvents_WhenOrderBySource_ShouldSortBySource() { // Arrange - var events = new List + var events = new List { EventUtils.CreateTestEvent(source: "ZSource"), EventUtils.CreateTestEvent(source: "ASource"), @@ -1008,7 +1008,7 @@ public void SortEvents_WhenOrderBySource_ShouldSortBySource() public void SortEvents_WhenOrderByTaskCategory_ShouldSortByTaskCategory() { // Arrange - var events = new List + var events = new List { EventUtils.CreateTestEvent(taskCategory: "ZCategory"), EventUtils.CreateTestEvent(taskCategory: "ACategory"), @@ -1028,7 +1028,7 @@ public void SortEvents_WhenOrderByTaskCategory_ShouldSortByTaskCategory() public void SortEvents_WhenOrderByThreadId_ShouldSortByThreadId() { // Arrange - var events = new List + var events = new List { EventUtils.CreateTestEvent(threadId: 30), EventUtils.CreateTestEvent(threadId: 10), @@ -1048,7 +1048,7 @@ public void SortEvents_WhenOrderByThreadId_ShouldSortByThreadId() public void SortEvents_WhenSingleItem_ShouldReturnSingleItem() { // Arrange - var events = new List { EventUtils.CreateTestEvent(100) }; + var events = new List { EventUtils.CreateTestEvent(100) }; // Act var result = events.SortEvents(ColumnName.EventId); @@ -1062,7 +1062,7 @@ public void SortEvents_WhenSingleItem_ShouldReturnSingleItem() public void SortEvents_WhenTiedOnPrimaryKeyDescending_ShouldBreakTieByRecordIdDescending() { // Arrange - var events = new List + var events = new List { EventUtils.CreateTestEvent(100, recordId: 1), EventUtils.CreateTestEvent(100, recordId: 3), @@ -1084,7 +1084,7 @@ public void SortEvents_WhenTiedOnPrimaryKey_ShouldBreakTieByRecordId() // Arrange - all events have the same TimeCreated but different RecordIds var baseTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc); - var events = new List + var events = new List { EventUtils.CreateTestEvent(timeCreated: baseTime, recordId: 3), EventUtils.CreateTestEvent(timeCreated: baseTime, recordId: 1), diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.cs index cde053f1..6abbb0d0 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.cs @@ -48,7 +48,7 @@ public void GetItems_LevelCategory_ReturnsAllSeverityLevelNames() [Fact] public void GetItems_LogDerivedCategory_ReturnsDistinctSortedValues() { - var events = new List + var events = new List { EventUtils.CreateTestEvent(id: 200, source: "Bravo"), EventUtils.CreateTestEvent(id: 100, source: "Alpha"), diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/FilterServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/FilterServiceTests.cs index 4a15a415..741af4be 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/FilterServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/FilterServiceTests.cs @@ -109,13 +109,13 @@ public void FilterActiveLogs_WhenMultipleLogs_ShouldFilterEachLog() // Arrange var filterService = CreateFilterService(); - var log1Events = new List + var log1Events = new List { EventUtils.CreateTestEvent(100, level: Constants.EventLevelError), EventUtils.CreateTestEvent(200, level: Constants.EventLevelInformation) }; - var log2Events = new List + var log2Events = new List { EventUtils.CreateTestEvent(100, level: Constants.EventLevelError), EventUtils.CreateTestEvent(300, level: Constants.EventLevelError) @@ -142,7 +142,7 @@ public void FilterActiveLogs_WhenNoFiltersEnabled_ShouldReturnAllEvents() // Arrange var filterService = CreateFilterService(); - var events = new List + var events = new List { EventUtils.CreateTestEvent(100), EventUtils.CreateTestEvent(200) @@ -235,7 +235,7 @@ public void GetFilteredEvents_WhenNoFiltersEnabled_ShouldReturnAllEvents() // Arrange var filterService = CreateFilterService(); - var events = new List + var events = new List { EventUtils.CreateTestEvent(100), EventUtils.CreateTestEvent(200), diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogEffectsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogEffectsTests.cs index ac0522d8..0eacd759 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogEffectsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogEffectsTests.cs @@ -80,8 +80,8 @@ public async Task HandleAddEvent_WhenContinuouslyUpdateTrue_AndEventFilteredOut_ CreateEffectsWithServices(true, activeLogs); // Mock the filter to drop everything (simulate "no events match the active filter"). - mockFilterService.GetFilteredEvents(Arg.Any>(), Arg.Any()) - .Returns(new List()); + mockFilterService.GetFilteredEvents(Arg.Any>(), Arg.Any()) + .Returns(new List()); var newEvent = EventUtils.CreateTestEvent(100, logName: Constants.LogNameTestLog); var action = new EventLogAction.AddEvent(newEvent); @@ -343,7 +343,7 @@ public async Task HandleLoadEvents_ShouldFilterAndDispatchUpdateTable() public async Task HandleLoadNewEvents_ShouldProcessBufferAndDispatchActions() { // Arrange - var bufferedEvents = new List + var bufferedEvents = new List { EventUtils.CreateTestEvent(100, logName: Constants.LogNameTestLog), EventUtils.CreateTestEvent(200, logName: Constants.LogNameTestLog) @@ -375,7 +375,7 @@ public async Task HandleLoadNewEvents_ShouldProcessBufferAndDispatchActions() public async Task HandleLoadNewEvents_WhenAllEventsFiltered_ShouldNotDispatchAppendBatch() { // Arrange - var bufferedEvents = new List + var bufferedEvents = new List { EventUtils.CreateTestEvent(100, logName: Constants.LogNameTestLog) }; @@ -386,8 +386,8 @@ public async Task HandleLoadNewEvents_WhenAllEventsFiltered_ShouldNotDispatchApp var (effects, mockDispatcher, _, _, mockFilterService) = CreateEffectsWithServices(activeLogs: activeLogs, newEventBuffer: bufferedEvents); - mockFilterService.GetFilteredEvents(Arg.Any>(), Arg.Any()) - .Returns(new List()); + mockFilterService.GetFilteredEvents(Arg.Any>(), Arg.Any()) + .Returns(new List()); // Act await effects.HandleLoadNewEvents(mockDispatcher); @@ -404,7 +404,7 @@ public async Task HandleLoadNewEvents_WhenBufferSpansMultipleLogs_ShouldGroupInt { // Arrange // EventUtils.CreateTestEvent always sets OwningLog="TestLog"; override via `with` to span 2 logs. - var bufferedEvents = new List + var bufferedEvents = new List { EventUtils.CreateTestEvent(100) with { OwningLog = Constants.LogNameApplication }, EventUtils.CreateTestEvent(200) with { OwningLog = Constants.LogNameTestLog }, @@ -634,7 +634,7 @@ public async Task HandleOpenLog_WhenNoEventResolver_ShouldDispatchError() public async Task HandleSetContinuouslyUpdate_WhenFalse_ShouldNotProcessBuffer() { // Arrange - var bufferedEvents = new List + var bufferedEvents = new List { EventUtils.CreateTestEvent(100, logName: Constants.LogNameTestLog) }; @@ -653,7 +653,7 @@ public async Task HandleSetContinuouslyUpdate_WhenFalse_ShouldNotProcessBuffer() public async Task HandleSetContinuouslyUpdate_WhenTrue_ShouldProcessBuffer() { // Arrange - var bufferedEvents = new List + var bufferedEvents = new List { EventUtils.CreateTestEvent(100, logName: Constants.LogNameTestLog) }; @@ -727,7 +727,7 @@ public async Task HandleSetFilters_FilterBranch_WhenLogClosedDuringFilter_Should // Arrange — log closes (ActiveLogs.Remove) while the filter task is running. The // post-filter dispatch must omit the closed log's slice. (The reducer also skips // unknown log ids, but checking at the effect keeps the dispatch minimal.) - var snapshotEvents = new List { EventUtils.CreateTestEvent(100) }; + var snapshotEvents = new List { EventUtils.CreateTestEvent(100) }; var snapshotData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, snapshotEvents); var snapshotState = new EventLogState @@ -745,7 +745,7 @@ public async Task HandleSetFilters_FilterBranch_WhenLogClosedDuringFilter_Should EventLogState volatileState = snapshotState; - var filterResult = new Dictionary> + var filterResult = new Dictionary> { [snapshotData.Id] = snapshotEvents }; @@ -776,7 +776,7 @@ public async Task HandleSetFilters_FilterBranch_WhenLogEventsChangeDuringFilter_ // Arrange — single open log; live event arrives during the first filter pass (Events ref // changes). The new filter must be re-applied to the post-mutation rows in a single retry // pass so the user sees the filter applied to the updated row set, not stale rows. - var snapshotEvents = new List { EventUtils.CreateTestEvent(100) }; + var snapshotEvents = new List { EventUtils.CreateTestEvent(100) }; var snapshotData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, snapshotEvents); var snapshotState = new EventLogState @@ -787,7 +787,7 @@ public async Task HandleSetFilters_FilterBranch_WhenLogEventsChangeDuringFilter_ AppliedFilter = new EventFilter(null, []) }; - var liveTailEvents = new List + var liveTailEvents = new List { EventUtils.CreateTestEvent(100), EventUtils.CreateTestEvent(101) @@ -802,12 +802,12 @@ public async Task HandleSetFilters_FilterBranch_WhenLogEventsChangeDuringFilter_ EventLogState volatileState = snapshotState; - var pass1Result = new Dictionary> + var pass1Result = new Dictionary> { [snapshotData.Id] = snapshotEvents }; - var pass2Result = new Dictionary> + var pass2Result = new Dictionary> { [snapshotData.Id] = liveTailEvents }; @@ -851,7 +851,7 @@ public async Task HandleSetFilters_FilterBranch_WhenLogStillStaleAfterRetry_Shou // Arrange — events change during pass 1 AND again during pass 2. Single-retry semantics // mean the pass-2 result is still stale; the slice must be omitted so the reducer's // preserve-omitted fallback keeps the existing rows (avoids losing live events). - var snapshotEvents = new List(); + var snapshotEvents = new List(); var snapshotData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, snapshotEvents); var snapshotState = new EventLogState @@ -862,8 +862,8 @@ public async Task HandleSetFilters_FilterBranch_WhenLogStillStaleAfterRetry_Shou AppliedFilter = new EventFilter(null, []) }; - var pass1MutationEvents = new List { EventUtils.CreateTestEvent(100) }; - var pass2MutationEvents = new List + var pass1MutationEvents = new List { EventUtils.CreateTestEvent(100) }; + var pass2MutationEvents = new List { EventUtils.CreateTestEvent(100), EventUtils.CreateTestEvent(101) @@ -885,12 +885,12 @@ public async Task HandleSetFilters_FilterBranch_WhenLogStillStaleAfterRetry_Shou EventLogState volatileState = snapshotState; - var pass1Result = new Dictionary> + var pass1Result = new Dictionary> { [snapshotData.Id] = [] }; - var pass2Result = new Dictionary> + var pass2Result = new Dictionary> { [snapshotData.Id] = pass1MutationEvents }; @@ -934,14 +934,14 @@ public async Task HandleSetFilters_FilterBranch_WhenSupersededByNewerFilter_Shou var (effects, mockDispatcher, _, _, mockFilterService) = CreateEffectsWithServices(activeLogs: activeLogs); - var staleResult = new Dictionary> + var staleResult = new Dictionary> { - [logData.Id] = new List { EventUtils.CreateTestEvent(999) } + [logData.Id] = new List { EventUtils.CreateTestEvent(999) } }; - var freshResult = new Dictionary> + var freshResult = new Dictionary> { - [logData.Id] = new List { EventUtils.CreateTestEvent(100) } + [logData.Id] = new List { EventUtils.CreateTestEvent(100) } }; var staleGate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -1014,7 +1014,7 @@ public async Task HandleSetFilters_ReloadBranch_ShouldNotDispatchSetIsLoading() public async Task HandleSetFilters_ShouldFilterAndDispatchUpdate() { // Arrange - var events = new List + var events = new List { EventUtils.CreateTestEvent(100, level: Constants.EventLevelError), EventUtils.CreateTestEvent(200, level: Constants.EventLevelInformation) @@ -1181,8 +1181,8 @@ public async Task HandleSetFilters_WhenFilterRequiresXml_ShouldRestoreSelectionA var mockFilterService = Substitute.For(); - mockFilterService.GetFilteredEvents(Arg.Any>(), Arg.Any()) - .Returns(callInfo => callInfo.Arg>().ToList()); + mockFilterService.GetFilteredEvents(Arg.Any>(), Arg.Any()) + .Returns(callInfo => callInfo.Arg>().ToList()); var mockServiceScopeFactory = Substitute.For(); var mockServiceScope = Substitute.For(); @@ -1350,7 +1350,7 @@ public void ReopenAfterDatabaseRemoval_DispatchesOpenLogPerSnapshotEntry() private static (EventLogEffects effects, IDispatcher mockDispatcher) CreateEffects( bool continuouslyUpdate = false, ImmutableDictionary? activeLogs = null, - List? newEventBuffer = null, + List? newEventBuffer = null, bool hasEventResolver = false) { var mockEventLogState = Substitute.For>(); @@ -1366,10 +1366,10 @@ private static (EventLogEffects effects, IDispatcher mockDispatcher) CreateEffec var mockFilterService = Substitute.For(); mockFilterService.FilterActiveLogs(Arg.Any>(), Arg.Any()) - .Returns(new Dictionary>()); + .Returns(new Dictionary>()); - mockFilterService.GetFilteredEvents(Arg.Any>(), Arg.Any()) - .Returns(callInfo => callInfo.Arg>().ToList()); + mockFilterService.GetFilteredEvents(Arg.Any>(), Arg.Any()) + .Returns(callInfo => callInfo.Arg>().ToList()); var mockLogger = Substitute.For(); var mockLogWatcherService = Substitute.For(); @@ -1479,10 +1479,10 @@ private static (EventLogEffects effects, var mockFilterService = Substitute.For(); mockFilterService.FilterActiveLogs(Arg.Any>(), Arg.Any()) - .Returns(new Dictionary>()); + .Returns(new Dictionary>()); - mockFilterService.GetFilteredEvents(Arg.Any>(), Arg.Any()) - .Returns(callInfo => callInfo.Arg>().ToList()); + mockFilterService.GetFilteredEvents(Arg.Any>(), Arg.Any()) + .Returns(callInfo => callInfo.Arg>().ToList()); var mockLogger = Substitute.For(); var mockLogWatcherService = Substitute.For(); @@ -1522,7 +1522,7 @@ private static (EventLogEffects effects, IFilterService mockFilterService) CreateEffectsWithServices( bool continuouslyUpdate = false, ImmutableDictionary? activeLogs = null, - List? newEventBuffer = null) + List? newEventBuffer = null) { var mockEventLogState = Substitute.For>(); @@ -1537,10 +1537,10 @@ private static (EventLogEffects effects, var mockFilterService = Substitute.For(); mockFilterService.FilterActiveLogs(Arg.Any>(), Arg.Any()) - .Returns(new Dictionary>()); + .Returns(new Dictionary>()); - mockFilterService.GetFilteredEvents(Arg.Any>(), Arg.Any()) - .Returns(callInfo => callInfo.Arg>().ToList()); + mockFilterService.GetFilteredEvents(Arg.Any>(), Arg.Any()) + .Returns(callInfo => callInfo.Arg>().ToList()); var mockLogger = Substitute.For(); var mockLogWatcherService = Substitute.For(); diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.cs index be27a754..1bac89f8 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.cs @@ -19,7 +19,7 @@ public void BufferedEventsWorkflow_ShouldHandleCorrectly() // Arrange var state = new EventLogState { ContinuouslyUpdate = false }; - var events = new List + var events = new List { EventUtils.CreateTestEvent(100), EventUtils.CreateTestEvent(200) @@ -43,7 +43,7 @@ public void BufferedEventsWorkflow_ShouldHandleCorrectly() public void EventLogAction_AddEventBuffered_ShouldStoreBufferAndFullFlag() { // Arrange - var events = new List + var events = new List { EventUtils.CreateTestEvent(100), EventUtils.CreateTestEvent(200) @@ -177,7 +177,7 @@ public void EventLogAction_SelectEvent_ShouldStoreEventAndSelectionFlags() public void EventLogAction_SelectEvents_ShouldStoreMultipleEvents() { // Arrange - var events = new List + var events = new List { EventUtils.CreateTestEvent(100), EventUtils.CreateTestEvent(200) @@ -221,7 +221,7 @@ public void EventLogData_Constructor_ShouldSetProperties() // Arrange var name = Constants.LogNameTestLog; var type = LogPathType.Channel; - var events = new List { EventUtils.CreateTestEvent(100) }; + var events = new List { EventUtils.CreateTestEvent(100) }; // Act var logData = new EventLogData(name, type, events); @@ -236,7 +236,7 @@ public void EventLogData_Constructor_ShouldSetProperties() public void EventLogData_GetCategoryValues_ForId_ShouldReturnDistinctIds() { // Arrange - var events = new List + var events = new List { EventUtils.CreateTestEvent(100), EventUtils.CreateTestEvent(100), @@ -271,7 +271,7 @@ public void EventLogData_GetCategoryValues_ForLevel_ShouldReturnAllSeverityLevel public void EventLogData_GetCategoryValues_ForSource_ShouldReturnDistinctSources() { // Arrange - var events = new List + var events = new List { EventUtils.CreateTestEvent(100), EventUtils.CreateTestEvent(200), @@ -425,7 +425,7 @@ public void ReduceAddEventBuffered_ShouldUpdateBufferAndFullFlag() // Arrange var state = new EventLogState(); - var events = new List + var events = new List { EventUtils.CreateTestEvent(100), EventUtils.CreateTestEvent(200) @@ -446,7 +446,7 @@ public void ReduceAddEventBuffered_WhenNotFull_ShouldSetFullFlagFalse() { // Arrange var state = new EventLogState { NewEventBufferIsFull = true }; - var events = new List { EventUtils.CreateTestEvent(100) }; + var events = new List { EventUtils.CreateTestEvent(100) }; var action = new EventLogAction.AddEventBuffered(events, false); // Act @@ -515,14 +515,14 @@ public void ReduceCloseAll_ShouldClearSelectedEvent() public void ReduceCloseLog_ShouldFilterNewEventBuffer() { // Arrange - var eventForLog1 = new DisplayEventModel(Constants.LogNameLog1, LogPathType.Channel) + var eventForLog1 = new ResolvedEvent(Constants.LogNameLog1, LogPathType.Channel) { Id = 100, Source = Constants.EventSourceTestSource, Level = Constants.EventLevelInformation }; - var eventForLog2 = new DisplayEventModel(Constants.LogNameLog2, LogPathType.Channel) + var eventForLog2 = new ResolvedEvent(Constants.LogNameLog2, LogPathType.Channel) { Id = 200, Source = Constants.EventSourceTestSource, @@ -914,7 +914,7 @@ public void ReduceSelectEvents_ShouldAddNewEventsOnly() var existingEvent = EventUtils.CreateTestEvent(100); var state = new EventLogState { SelectedEvents = [existingEvent] }; - var newEvents = new List + var newEvents = new List { existingEvent, // Already selected EventUtils.CreateTestEvent(200) // New diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EventTableStoreTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EventTableStoreTests.cs index 94b2e2ea..70bcdb87 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EventTableStoreTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EventTableStoreTests.cs @@ -127,9 +127,9 @@ public void EventTableAction_UpdateDisplayedEvents_ShouldStoreActiveLogs() { // Arrange var logId = EventLogId.Create(); - var events = new List { EventUtils.CreateTestEvent(100) }; + var events = new List { EventUtils.CreateTestEvent(100) }; - var activeLogs = new Dictionary> + var activeLogs = new Dictionary> { { logId, events } }; @@ -148,7 +148,7 @@ public void EventTableAction_UpdateTable_ShouldStoreLogIdAndEvents() // Arrange var logId = EventLogId.Create(); - var events = new List + var events = new List { EventUtils.CreateTestEvent(100), EventUtils.CreateTestEvent(200) @@ -170,7 +170,7 @@ public void EventTableModel_ComputerName_AfterFirstEventArrives_ShouldBeStoredOn var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); - var firstBatch = new List + var firstBatch = new List { new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 10, RecordId = 1, ComputerName = Constants.EventComputerServer01 } }; @@ -179,7 +179,7 @@ public void EventTableModel_ComputerName_AfterFirstEventArrives_ShouldBeStoredOn state, new EventTableAction.AppendTableEvents(logData.Id, firstBatch)); - var secondBatch = new List + var secondBatch = new List { new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 11, RecordId = 2, ComputerName = Constants.EventComputerServer02 } }; @@ -202,7 +202,7 @@ public void EventTableModel_ComputerName_WhenFirstEventHasEmptyComputerName_Shou var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); - var batch = new List + var batch = new List { new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 10, RecordId = 1, ComputerName = string.Empty }, new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 11, RecordId = 2, ComputerName = Constants.EventComputerServer01 } @@ -294,7 +294,7 @@ public void IntegrationTest_LoadAndUpdateTableEvents() Assert.True(state.EventTables.First().IsLoading); // Act - Update table with events - var events = new List + var events = new List { EventUtils.CreateTestEvent(100), EventUtils.CreateTestEvent(200) @@ -468,7 +468,7 @@ public void ReduceAppendTableEventsBatch_WhenBatchTargetsClosedLog_ShouldSkipTha state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(openLog)); var staleLogId = EventLogId.Create(); - var batches = new Dictionary> + var batches = new Dictionary> { { openLog.Id, [new(Constants.LogNameLog1, LogPathType.Channel) { Id = 10, RecordId = 1 }] }, { staleLogId, [new("ClosedLog", LogPathType.Channel) { Id = 99, RecordId = 99 }] } @@ -494,7 +494,7 @@ public void ReduceAppendTableEvents_ShouldAppendEventsToExistingDisplayedEvents( var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); - var initialEvents = new List + var initialEvents = new List { EventUtils.CreateTestEvent(100, recordId: 1), EventUtils.CreateTestEvent(200, recordId: 2) @@ -504,7 +504,7 @@ public void ReduceAppendTableEvents_ShouldAppendEventsToExistingDisplayedEvents( state, new EventTableAction.AppendTableEvents(logData.Id, initialEvents)); - var deltaEvents = new List + var deltaEvents = new List { EventUtils.CreateTestEvent(300, recordId: 3), EventUtils.CreateTestEvent(400, recordId: 4) @@ -534,7 +534,7 @@ public void ReduceAppendTableEvents_ShouldNotChangeIsLoading() var action = new EventTableAction.AppendTableEvents( logData.Id, - new List { EventUtils.CreateTestEvent(100, recordId: 1) }); + new List { EventUtils.CreateTestEvent(100, recordId: 1) }); // Act var newState = EventTableReducers.ReduceAppendTableEvents(state, action); @@ -553,7 +553,7 @@ public void ReduceAppendTableEvents_ShouldPreserveSortOrder() var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); - var initialEvents = new List + var initialEvents = new List { EventUtils.CreateTestEvent(100, recordId: 10), EventUtils.CreateTestEvent(200, recordId: 20) @@ -564,7 +564,7 @@ public void ReduceAppendTableEvents_ShouldPreserveSortOrder() new EventTableAction.AppendTableEvents(logData.Id, initialEvents)); // Append events with record IDs that should sort between and after existing - var deltaEvents = new List + var deltaEvents = new List { EventUtils.CreateTestEvent(300, recordId: 5), EventUtils.CreateTestEvent(400, recordId: 15) @@ -596,7 +596,7 @@ public void ReduceAppendTableEvents_WhenTableNotFound_ShouldReturnUnchangedState var action = new EventTableAction.AppendTableEvents( unknownLogId, - new List { EventUtils.CreateTestEvent(100) }); + new List { EventUtils.CreateTestEvent(100) }); // Act var newState = EventTableReducers.ReduceAppendTableEvents(state, action); @@ -634,18 +634,18 @@ public void ReduceCloseLog_ShouldScrubCanonicalRowsAndCountForClosedLog() state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData3)); - var log1Events = new List + var log1Events = new List { new(Constants.LogNameLog1, LogPathType.Channel) { Id = 100, RecordId = 1 } }; - var log2Events = new List + var log2Events = new List { new(Constants.LogNameLog2, LogPathType.Channel) { Id = 200, RecordId = 1 }, new(Constants.LogNameLog2, LogPathType.Channel) { Id = 201, RecordId = 2 } }; - var log3Events = new List + var log3Events = new List { new(Constants.LogNameLog3, LogPathType.Channel) { Id = 300, RecordId = 1 } }; @@ -677,12 +677,12 @@ public void ReduceCloseLog_WhenClosingDownToOneLog_ShouldKeepOnlySoleRemainingLo state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); - var log1Events = new List + var log1Events = new List { new(Constants.LogNameLog1, LogPathType.Channel) { Id = 100, RecordId = 1 } }; - var log2Events = new List + var log2Events = new List { new(Constants.LogNameLog2, LogPathType.Channel) { Id = 200, RecordId = 1 }, new(Constants.LogNameLog2, LogPathType.Channel) { Id = 201, RecordId = 2 } @@ -1003,7 +1003,7 @@ public void ReduceSetOrderBy_WhenToggledOff_ShouldSortPerLogListsByEffectiveComp var state = new EventTableState { OrderBy = ColumnName.Level, IsDescending = false }; state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); - var events = new List + var events = new List { new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 10, RecordId = 1, TimeCreated = baseTime.AddSeconds(40) }, new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 11, RecordId = 2, TimeCreated = baseTime.AddSeconds(20) } @@ -1111,7 +1111,7 @@ public void ReduceToggleSorting_WhenOrderByIsNull_ShouldPreserveNullOrderBy() var state = new EventTableState { OrderBy = null, IsDescending = false }; state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); - var events = new List + var events = new List { new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 10, RecordId = 1, TimeCreated = baseTime.AddSeconds(40) }, new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 11, RecordId = 2, TimeCreated = baseTime.AddSeconds(20) } @@ -1141,17 +1141,17 @@ public void ReduceUpdateDisplayedEvents_ShouldUpdateSlicesForLogsInActiveLogs() state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); - var log1Events = new List + var log1Events = new List { new(Constants.LogNameLog1, LogPathType.Channel) { Id = 10, RecordId = 1 } }; - var log2Events = new List + var log2Events = new List { new(Constants.LogNameLog2, LogPathType.Channel) { Id = 20, RecordId = 2 } }; - var activeLogs = new Dictionary> + var activeLogs = new Dictionary> { { logData1.Id, log1Events }, { logData2.Id, log2Events } @@ -1177,17 +1177,17 @@ public void ReduceUpdateDisplayedEvents_WhenActiveLogsContainsClosedLogId_Should state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(openLog)); var staleLogId = EventLogId.Create(); - var openLogEvents = new List + var openLogEvents = new List { new(Constants.LogNameLog1, LogPathType.Channel) { Id = 10, RecordId = 1 } }; - var staleEvents = new List + var staleEvents = new List { new("ClosedLog", LogPathType.Channel) { Id = 99, RecordId = 99 } }; - var activeLogs = new Dictionary> + var activeLogs = new Dictionary> { { openLog.Id, openLogEvents }, { staleLogId, staleEvents } @@ -1215,7 +1215,7 @@ public void ReduceUpdateDisplayedEvents_WhenLogIsNotInActiveLogs_ShouldPreserveE var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); - var loadedEvents = new List + var loadedEvents = new List { new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 10, RecordId = 1 }, new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 11, RecordId = 2 } @@ -1223,7 +1223,7 @@ public void ReduceUpdateDisplayedEvents_WhenLogIsNotInActiveLogs_ShouldPreserveE state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData.Id, loadedEvents)); - var emptyActiveLogs = new Dictionary>(); + var emptyActiveLogs = new Dictionary>(); var action = new EventTableAction.UpdateDisplayedEvents(emptyActiveLogs); // Act @@ -1247,13 +1247,13 @@ public void ReduceUpdateDisplayedEvents_WhenSomeLogsOmitted_ShouldReplaceInclude state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); - var log1Loaded = new List + var log1Loaded = new List { new(Constants.LogNameLog1, LogPathType.Channel) { Id = 100, RecordId = 1 }, new(Constants.LogNameLog1, LogPathType.Channel) { Id = 101, RecordId = 2 } }; - var log2Loaded = new List + var log2Loaded = new List { new(Constants.LogNameLog2, LogPathType.Channel) { Id = 200, RecordId = 1 }, new(Constants.LogNameLog2, LogPathType.Channel) { Id = 201, RecordId = 2 }, @@ -1263,12 +1263,12 @@ public void ReduceUpdateDisplayedEvents_WhenSomeLogsOmitted_ShouldReplaceInclude state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData1.Id, log1Loaded)); state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData2.Id, log2Loaded)); - var log1Filtered = new List + var log1Filtered = new List { new(Constants.LogNameLog1, LogPathType.Channel) { Id = 100, RecordId = 1 } }; - var activeLogs = new Dictionary> + var activeLogs = new Dictionary> { { logData1.Id, log1Filtered } }; @@ -1294,13 +1294,13 @@ public void ReduceUpdateDisplayedEvents_WhenTableComputerNameEmpty_ShouldLatchFr var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); - var revealedEvents = new List + var revealedEvents = new List { new(Constants.LogNameLog1, LogPathType.Channel) { Id = 10, RecordId = 1, ComputerName = string.Empty }, new(Constants.LogNameLog1, LogPathType.Channel) { Id = 11, RecordId = 2, ComputerName = Constants.EventComputerServer01 } }; - var activeLogs = new Dictionary> + var activeLogs = new Dictionary> { { logData.Id, revealedEvents } }; @@ -1323,7 +1323,7 @@ public void ReduceUpdateTable_AfterPartialAppends_ShouldReplaceNotMergeWithParti var state = new EventTableState { IsDescending = false }; state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); - var partial1 = new List + var partial1 = new List { new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 10, RecordId = 1 }, new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 11, RecordId = 2 } @@ -1333,7 +1333,7 @@ public void ReduceUpdateTable_AfterPartialAppends_ShouldReplaceNotMergeWithParti state, new EventTableAction.AppendTableEvents(logData.Id, partial1)); - var partial2 = new List + var partial2 = new List { new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 12, RecordId = 3 } }; @@ -1344,7 +1344,7 @@ public void ReduceUpdateTable_AfterPartialAppends_ShouldReplaceNotMergeWithParti Assert.Equal(3, state.DisplayedEvents.Count); - var fullLoad = new List + var fullLoad = new List { new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 20, RecordId = 1 }, new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 21, RecordId = 2 }, @@ -1371,7 +1371,7 @@ public void ReduceUpdateTable_ShouldUpdateTableEvents() var state = new EventTableState(); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); - var events = new List + var events = new List { EventUtils.CreateTestEvent(100), EventUtils.CreateTestEvent(200) @@ -1397,7 +1397,7 @@ public void ReduceUpdateTable_WhenCalledTwiceForSameLog_ShouldReplaceNotDuplicat var state = new EventTableState { IsDescending = false }; state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); - var firstLoad = new List + var firstLoad = new List { new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 10, RecordId = 1 }, new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 11, RecordId = 2 } @@ -1406,7 +1406,7 @@ public void ReduceUpdateTable_WhenCalledTwiceForSameLog_ShouldReplaceNotDuplicat state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData.Id, firstLoad)); Assert.Equal(2, state.DisplayedEvents.Count); - var secondLoad = new List + var secondLoad = new List { new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 12, RecordId = 3 }, new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 13, RecordId = 4 }, @@ -1432,13 +1432,13 @@ public void ReduceUpdateTable_WhenDescendingOrderRequested_ShouldMergeInDescendi state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); - var eventsLog1 = new List + var eventsLog1 = new List { new(Constants.LogNameLog1, LogPathType.Channel) { Id = 10, RecordId = 1 }, new(Constants.LogNameLog1, LogPathType.Channel) { Id = 11, RecordId = 3 } }; - var eventsLog2 = new List + var eventsLog2 = new List { new(Constants.LogNameLog2, LogPathType.Channel) { Id = 20, RecordId = 2 }, new(Constants.LogNameLog2, LogPathType.Channel) { Id = 21, RecordId = 4 } @@ -1466,7 +1466,7 @@ public void ReduceUpdateTable_WhenSecondLogIsEmpty_ShouldKeepFirstLogEvents() state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); - var eventsLog1 = new List + var eventsLog1 = new List { new(Constants.LogNameLog1, LogPathType.Channel) { Id = 10, RecordId = 5 }, new(Constants.LogNameLog1, LogPathType.Channel) { Id = 11, RecordId = 7 } @@ -1492,13 +1492,13 @@ public void ReduceUpdateTable_WhenSecondLogPopulated_ShouldMergeIntoCanonicalInS state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); - var eventsLog1 = new List + var eventsLog1 = new List { new(Constants.LogNameLog1, LogPathType.Channel) { Id = 10, RecordId = 1 }, new(Constants.LogNameLog1, LogPathType.Channel) { Id = 11, RecordId = 3 } }; - var eventsLog2 = new List + var eventsLog2 = new List { new(Constants.LogNameLog2, LogPathType.Channel) { Id = 20, RecordId = 2 }, new(Constants.LogNameLog2, LogPathType.Channel) { Id = 21, RecordId = 4 } @@ -1524,7 +1524,7 @@ public void ReduceUpdateTable_WhenTableNotFound_ShouldReturnStateUnchanged() // Arrange — empty state, no tables var state = new EventTableState(); var staleLogId = EventLogId.Create(); - var events = new List { EventUtils.CreateTestEvent(100) }; + var events = new List { EventUtils.CreateTestEvent(100) }; var action = new EventTableAction.UpdateTable(staleLogId, events); // Act — stale UpdateTable for a non-existent table @@ -1540,13 +1540,13 @@ public void ReduceUpdateTable_WhenTimeCreatedDivergesFromRecordId_ShouldMergeByT // Arrange — RecordIds ascending but TimeCreated descending: verifies sort is by timestamp var baseTime = new DateTime(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc); - var log1Events = new List + var log1Events = new List { new(Constants.LogNameLog1, LogPathType.Channel) { Id = 10, RecordId = 1, TimeCreated = baseTime.AddSeconds(40) }, new(Constants.LogNameLog1, LogPathType.Channel) { Id = 11, RecordId = 2, TimeCreated = baseTime.AddSeconds(20) } }; - var log2Events = new List + var log2Events = new List { new(Constants.LogNameLog2, LogPathType.Channel) { Id = 20, RecordId = 1, TimeCreated = baseTime.AddSeconds(30) }, new(Constants.LogNameLog2, LogPathType.Channel) { Id = 21, RecordId = 2, TimeCreated = baseTime.AddSeconds(10) } diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/FilterPane/FilterPaneEffectsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/FilterPane/FilterPaneEffectsTests.cs index ce7c489b..a12ff6b9 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/FilterPane/FilterPaneEffectsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/FilterPane/FilterPaneEffectsTests.cs @@ -226,7 +226,7 @@ public async Task HandleSetFilterDateRange_WhenAfterIsNull_ShouldUseEnvelopeFrom var newest = new DateTime(2024, 1, 1, 14, 15, 0, DateTimeKind.Utc); var unrelatedBefore = new DateTime(2024, 1, 1, 23, 0, 0, DateTimeKind.Utc); - var events = new List + var events = new List { EventUtils.CreateTestEvent(timeCreated: newest), EventUtils.CreateTestEvent(timeCreated: oldest) @@ -257,7 +257,7 @@ public async Task HandleSetFilterDateRange_WhenBeforeIsNull_ShouldUseEnvelopeFro var newest = new DateTime(2024, 1, 1, 14, 15, 0, DateTimeKind.Utc); var unrelatedAfter = new DateTime(2023, 12, 1, 0, 0, 0, DateTimeKind.Utc); - var events = new List + var events = new List { EventUtils.CreateTestEvent(timeCreated: newest), EventUtils.CreateTestEvent(timeCreated: oldest) diff --git a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/EventUtils.cs b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/EventUtils.cs index 94c41601..aa8f6364 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/EventUtils.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/EventUtils.cs @@ -8,7 +8,7 @@ namespace EventLogExpert.UI.Tests.TestUtils; internal static class EventUtils { - internal static DisplayEventModel CreateTestEvent( + internal static ResolvedEvent CreateTestEvent( int id = 1, string source = "TestSource", string level = "Information",