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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions EventLogExpert.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
<Project Path="tests/Unit/EventLogExpert.UI.Tests/EventLogExpert.UI.Tests.csproj" />
</Folder>
<Folder Name="/tests/Integration/">
<Project Path="tests/Integration/EventLogExpert.EventDbTool.IntegrationTests/EventLogExpert.EventDbTool.IntegrationTests.csproj" />
<Project Path="tests/Integration/EventLogExpert.Eventing.IntegrationTests/EventLogExpert.Eventing.IntegrationTests.csproj" />
<Project Path="tests/Integration/EventLogExpert.UI.IntegrationTests/EventLogExpert.UI.IntegrationTests.csproj" />
</Folder>
<Folder Name="/tests/Shared/">
<Project Path="tests/Shared/EventLogExpert.Eventing.TestUtils/EventLogExpert.Eventing.TestUtils.csproj" />
</Folder>
</Solution>
2 changes: 1 addition & 1 deletion src/EventLogExpert.EventDbTool/CreateDatabaseCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public static Command GetCommand()
return createDatabaseCommand;
}

private void CreateDatabase(string path, string? source, string? filter, string? skipProvidersInFile)
internal void CreateDatabase(string path, string? source, string? filter, string? skipProvidersInFile)
{
if (File.Exists(path))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("EventLogExpert.EventDbTool.IntegrationTests")]
[assembly: InternalsVisibleTo("EventLogExpert.EventDbTool.Tests")]
2 changes: 1 addition & 1 deletion src/EventLogExpert.EventDbTool/ShowCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public static Command GetCommand()
return showCommand;
}

private void ShowProviderInfo(string? source, string? filter)
internal void ShowProviderInfo(string? source, string? filter)
{
if (!RegexHelper.TryCreate(filter, Logger, out var regex)) { return; }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

using EventLogExpert.EventDbTool.IntegrationTests.TestUtils;
using EventLogExpert.EventDbTool.IntegrationTests.TestUtils.Constants;
using EventLogExpert.Eventing.Logging;
using EventLogExpert.Eventing.ProviderDatabase;
using NSubstitute;

namespace EventLogExpert.EventDbTool.IntegrationTests;

public sealed class CreateDatabaseCommandTests : IDisposable
{
private readonly List<string> _tempDirs = [];
private readonly List<string> _tempPaths = [];

[Fact]
public void CreateDatabase_WhenExtensionNotDb_LogsErrorAndDoesNotCreateFile()
{
// Arrange
var path = DatabaseTestUtils.CreateTempPath(".txt");
_tempPaths.Add(path);
var logger = Substitute.For<ITraceLogger>();

// Act
new CreateDatabaseCommand(logger).CreateDatabase(path, source: null, filter: null, skipProvidersInFile: null);

// Assert
Assert.False(File.Exists(path), "No file should be written when the extension is wrong.");
logger.Received(1).Error(Arg.Is<ErrorLogHandler>(h =>
h.ToString().Contains("File extension must be .db")));
}

[Fact]
public void CreateDatabase_WhenFilterMatchesNoProviders_DoesNotLeaveEmptyDatabaseOnDisk()
{
// Arrange — source has First+Second, filter excludes both. The command must not leave an
// empty .db file behind, because a downstream consumer would read it as "this collection
// has zero providers" instead of "this provider set was never collected".
var source = CreateTempPath();
DatabaseTestUtils.CreateV4Database(source,
DatabaseTestUtils.BuildProviderDetails(Constants.FirstProviderName),
DatabaseTestUtils.BuildProviderDetails(Constants.SecondProviderName));

var path = CreateTempPath();
var logger = Substitute.For<ITraceLogger>();

// Act
new CreateDatabaseCommand(logger).CreateDatabase(path, source, filter: "ZZZ_NoMatch_ZZZ", skipProvidersInFile: null);

// Assert
Assert.False(File.Exists(path), "No file should be written when zero providers were resolved.");
logger.Received(1).Warning(Arg.Is<WarningLogHandler>(h =>
h.ToString().Contains("No provider details could be resolved") &&
h.ToString().Contains("Database was not created")));
// No errors should have been logged on this path; an extra error here would be a UX regression.
logger.DidNotReceive().Error(Arg.Any<ErrorLogHandler>());
}

[Fact]
public void CreateDatabase_WhenFilterRegexInvalid_LogsErrorAndDoesNotCreateFile()
{
// Arrange
var path = CreateTempPath();
var logger = Substitute.For<ITraceLogger>();

// Act
new CreateDatabaseCommand(logger).CreateDatabase(path, source: null, filter: "[unclosed", skipProvidersInFile: null);

// Assert
Assert.False(File.Exists(path), "No file should be written when the filter is invalid.");
logger.Received(1).Error(Arg.Is<ErrorLogHandler>(h =>
h.ToString().Contains("Invalid --filter regex")));
}

[Fact]
public void CreateDatabase_WhenProviderCountCrossesBatchSize_PersistsEveryProviderWithoutErrors()
{
// Arrange — exercises the mid-stream FlushHeaderAndBuffer path: at 100 providers the buffer
// is flushed, the DbContext is materialized, and subsequent providers are appended in
// BatchSize=100 chunks. 101 providers guarantees we cross the boundary AND continue beyond
// it, so a regression in the "after first flush" branch would lose providers.
const int ProviderCount = 101;
var source = CreateTempPath();
var providers = Enumerable.Range(0, ProviderCount)
.Select(i => DatabaseTestUtils.BuildProviderDetails($"Provider-{i:D4}"))
.ToArray();
DatabaseTestUtils.CreateV4Database(source, providers);

var path = CreateTempPath();
var logger = Substitute.For<ITraceLogger>();

// Act
new CreateDatabaseCommand(logger).CreateDatabase(path, source, filter: null, skipProvidersInFile: null);

// Assert
Assert.True(File.Exists(path));

using var verify = new ProviderDbContext(path, true);
Assert.Equal(ProviderCount, verify.ProviderDetails.Count());
var firstName = verify.ProviderDetails.OrderBy(r => r.ProviderName).First().ProviderName;
var lastName = verify.ProviderDetails.OrderBy(r => r.ProviderName).Last().ProviderName;
Assert.Equal("Provider-0000", firstName);
Assert.Equal($"Provider-{ProviderCount - 1:D4}", lastName);
logger.DidNotReceive().Error(Arg.Any<ErrorLogHandler>());
logger.DidNotReceive().Warning(Arg.Any<WarningLogHandler>());
}

[Fact]
public void CreateDatabase_WhenSkipProvidersInFileExcludesAll_DoesNotLeaveEmptyDatabaseOnDisk()
{
// Arrange — distinct contract path from the filter case: the skip-source contains every
// provider in the source, leaving zero to write. The "no empty .db" guarantee must hold
// here too; a regression in the skip-set integration could reintroduce the empty stub.
var source = CreateTempPath();
DatabaseTestUtils.CreateV4Database(source,
DatabaseTestUtils.BuildProviderDetails(Constants.FirstProviderName),
DatabaseTestUtils.BuildProviderDetails(Constants.SecondProviderName));

var skipSource = CreateTempPath();
DatabaseTestUtils.CreateV4Database(skipSource,
DatabaseTestUtils.BuildProviderDetails(Constants.FirstProviderName),
DatabaseTestUtils.BuildProviderDetails(Constants.SecondProviderName));

var path = CreateTempPath();
var logger = Substitute.For<ITraceLogger>();

// Act
new CreateDatabaseCommand(logger).CreateDatabase(path, source, filter: null, skipProvidersInFile: skipSource);

// Assert
Assert.False(File.Exists(path), "No file should be written when the skip-set excludes all providers.");
logger.Received(1).Warning(Arg.Is<WarningLogHandler>(h =>
h.ToString().Contains("No provider details could be resolved") &&
h.ToString().Contains("Database was not created")));
logger.DidNotReceive().Error(Arg.Any<ErrorLogHandler>());
}

[Fact]
public void CreateDatabase_WhenSkipProvidersInFileResolves_ExcludesThoseProvidersFromOutput()
{
// Arrange — source has First+Second+Shared. Skip-source has Shared. Output must contain
// First+Second only, exercising the skip-set integration with the streaming write path.
var source = CreateTempPath();
DatabaseTestUtils.CreateV4Database(source,
DatabaseTestUtils.BuildProviderDetails(Constants.FirstProviderName),
DatabaseTestUtils.BuildProviderDetails(Constants.SecondProviderName),
DatabaseTestUtils.BuildProviderDetails(Constants.SharedProviderName));

var skipSource = CreateTempPath();
DatabaseTestUtils.CreateV4Database(skipSource,
DatabaseTestUtils.BuildProviderDetails(Constants.SharedProviderName));

var path = CreateTempPath();
var logger = Substitute.For<ITraceLogger>();

// Act
new CreateDatabaseCommand(logger).CreateDatabase(path, source, filter: null, skipProvidersInFile: skipSource);

// Assert
Assert.True(File.Exists(path));

using var verify = new ProviderDbContext(path, true);
var names = verify.ProviderDetails.Select(r => r.ProviderName).OrderBy(n => n).ToList();
Assert.Equal(new[] { Constants.FirstProviderName, Constants.SecondProviderName }, names);
logger.DidNotReceive().Error(Arg.Any<ErrorLogHandler>());
logger.DidNotReceive().Warning(Arg.Any<WarningLogHandler>());
}

[Fact]
public void CreateDatabase_WhenSkipProvidersInFileSourceDoesNotExist_LogsErrorAndDoesNotCreateFile()
{
// Arrange — valid source but invalid skip-source. Validation order matters: a missing skip
// file must fail BEFORE we begin writing the output database, so an empty stub is never
// left behind.
var path = CreateTempPath();
var source = CreateTempPath();
DatabaseTestUtils.CreateV4Database(source,
DatabaseTestUtils.BuildProviderDetails(Constants.FirstProviderName));

var missingSkipSource = DatabaseTestUtils.CreateTempPath(".db");
var logger = Substitute.For<ITraceLogger>();

// Act
new CreateDatabaseCommand(logger).CreateDatabase(path, source, filter: null, skipProvidersInFile: missingSkipSource);

// Assert
Assert.False(File.Exists(path), "No file should be written when the skip-source is missing.");
logger.Received(1).Error(Arg.Is<ErrorLogHandler>(h =>
h.ToString().Contains("Source not found") && h.ToString().Contains(missingSkipSource)));
}

[Fact]
public void CreateDatabase_WhenSourceDoesNotExist_LogsErrorAndDoesNotCreateFile()
{
// Arrange
var path = CreateTempPath();
var missingSource = DatabaseTestUtils.CreateTempPath(".db");
var logger = Substitute.For<ITraceLogger>();

// Act
new CreateDatabaseCommand(logger).CreateDatabase(path, source: missingSource, filter: null, skipProvidersInFile: null);

// Assert
Assert.False(File.Exists(path), "No file should be written when the source is missing.");
logger.Received(1).Error(Arg.Is<ErrorLogHandler>(h =>
h.ToString().Contains("Source not found") && h.ToString().Contains(missingSource)));
}

[Fact]
public void CreateDatabase_WhenSourceProvidersResolved_PersistsAllProvidersAndPreservesOwningPublisher()
{
// Arrange — one provider has ResolvedFromOwningPublisher set; this round-trips through the
// full streaming write path and we verify the persisted DB matches the source contents.
var source = CreateTempPath();
DatabaseTestUtils.CreateV4Database(source,
DatabaseTestUtils.BuildProviderDetails(Constants.FirstProviderName),
DatabaseTestUtils.BuildProviderDetails(Constants.SecondProviderName, Constants.OwningPublisherName));

var path = CreateTempPath();
var logger = Substitute.For<ITraceLogger>();

// Act
new CreateDatabaseCommand(logger).CreateDatabase(path, source, filter: null, skipProvidersInFile: null);

// Assert
Assert.True(File.Exists(path), "Output database should be created when providers are resolved.");

using var verify = new ProviderDbContext(path, true);
var rows = verify.ProviderDetails.OrderBy(r => r.ProviderName).ToList();
Assert.Equal(2, rows.Count);
Assert.Equal(Constants.FirstProviderName, rows[0].ProviderName);
Assert.Null(rows[0].ResolvedFromOwningPublisher);
Assert.Equal(Constants.SecondProviderName, rows[1].ProviderName);
Assert.Equal(Constants.OwningPublisherName, rows[1].ResolvedFromOwningPublisher);
// Success path must not surface any errors or warnings; a regression that warned spuriously
// (e.g., "no providers resolved" reaching the success branch) would degrade operator trust.
logger.DidNotReceive().Error(Arg.Any<ErrorLogHandler>());
logger.DidNotReceive().Warning(Arg.Any<WarningLogHandler>());
}

[Fact]
public void CreateDatabase_WhenTargetFileAlreadyExists_LogsErrorAndDoesNotOverwrite()
{
// Arrange — target file already exists with sentinel bytes; the command must not overwrite or truncate.
var path = CreateTempPath();
var sentinel = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF };
File.WriteAllBytes(path, sentinel);
var logger = Substitute.For<ITraceLogger>();

// Act
new CreateDatabaseCommand(logger).CreateDatabase(path, source: null, filter: null, skipProvidersInFile: null);

// Assert
Assert.Equal(sentinel, File.ReadAllBytes(path));
logger.Received(1).Error(Arg.Is<ErrorLogHandler>(h =>
h.ToString().Contains("file already exists") && h.ToString().Contains(path)));
}

public void Dispose()
{
foreach (var path in _tempPaths)
{
DatabaseTestUtils.DeleteDatabaseFile(path);
}

foreach (var dir in _tempDirs)
{
DatabaseTestUtils.DeleteDirectoryRecursive(dir);
}
}

private string CreateTempPath()
{
var path = DatabaseTestUtils.CreateTempPath();
_tempPaths.Add(path);

return path;
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

using EventLogExpert.EventDbTool.Tests.TestUtils;
using EventLogExpert.EventDbTool.Tests.TestUtils.Constants;
using EventLogExpert.EventDbTool.IntegrationTests.TestUtils;
using EventLogExpert.EventDbTool.IntegrationTests.TestUtils.Constants;
using EventLogExpert.Eventing.Logging;
using EventLogExpert.Eventing.ProviderDatabase;
using NSubstitute;

namespace EventLogExpert.EventDbTool.Tests;
namespace EventLogExpert.EventDbTool.IntegrationTests;

public sealed class DiffDatabaseCommandTests : IDisposable
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\EventLogExpert.EventDbTool\EventLogExpert.EventDbTool.csproj" />
<ProjectReference Include="..\..\..\src\EventLogExpert.Eventing\EventLogExpert.Eventing.csproj" />
<ProjectReference Include="..\..\Shared\EventLogExpert.Eventing.TestUtils\EventLogExpert.Eventing.TestUtils.csproj" />
</ItemGroup>

<!--
Force xunit.runner.json next to the test assembly so the runner picks up the
serial-execution config; without CopyToOutputDirectory the file can be silently ignored
depending on the SDK / runner version.
-->
<ItemGroup>
<None Update="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

global using Xunit;
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

using EventLogExpert.EventDbTool.Tests.TestUtils;
using EventLogExpert.EventDbTool.Tests.TestUtils.Constants;
using EventLogExpert.EventDbTool.IntegrationTests.TestUtils;
using EventLogExpert.EventDbTool.IntegrationTests.TestUtils.Constants;
using EventLogExpert.Eventing.Logging;
using NSubstitute;

namespace EventLogExpert.EventDbTool.Tests;
namespace EventLogExpert.EventDbTool.IntegrationTests;

public sealed class MergeDatabaseCommandTests : IDisposable
{
Expand Down
Loading
Loading