Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use MSBuild Logging APIs for Task output instead of StdOut #933

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Ensure that for all TFMs we can infer a verbosity and apply it to the…
… SBOM tool
  • Loading branch information
baronfel committed Mar 1, 2025
commit 9a3c6ae3a4a2294eda10d42482ec65a0c52391a6
82 changes: 45 additions & 37 deletions src/Microsoft.Sbom.Targets/GenerateSbomTask.cs
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ namespace Microsoft.Sbom.Targets;

using System;
using System.Collections.Generic;
using System.Diagnostics.Tracing;
using Microsoft.Build.Utilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -28,53 +29,49 @@ namespace Microsoft.Sbom.Targets;
/// </summary>
public partial class GenerateSbom : Task
{
private readonly IHost taskHost;

/// <summary>
/// Constructor for the GenerateSbomTask.
/// </summary>
public GenerateSbom()
{
var taskLoggingHelper = new TaskLoggingHelper(this);
taskHost = Host.CreateDefaultBuilder()
.ConfigureServices((host, services) =>
services
.AddSbomTool(LogEventLevel.Information, taskLoggingHelper)
/* Manually adding some dependencies since `AddSbomTool()` does not add them when
* running the MSBuild Task from another project.
*/
.AddSingleton<ISourcesProvider, SbomPackagesProvider>()
.AddSingleton<ISourcesProvider, CGExternalDocumentReferenceProvider>()
.AddSingleton<ISourcesProvider, DirectoryTraversingFileToJsonProvider>()
.AddSingleton<ISourcesProvider, ExternalDocumentReferenceFileProvider>()
.AddSingleton<ISourcesProvider, ExternalDocumentReferenceProvider>()
.AddSingleton<ISourcesProvider, FileListBasedFileToJsonProvider>()
.AddSingleton<ISourcesProvider, SbomFileBasedFileToJsonProvider>()
.AddSingleton<ISourcesProvider, CGScannedExternalDocumentReferenceFileProvider>()
.AddSingleton<ISourcesProvider, CGScannedPackagesProvider>()
.AddSingleton<IAlgorithmNames, AlgorithmNames>()
.AddSingleton<IManifestGenerator, SPDX22.Generator>()
.AddSingleton<IManifestGenerator, SPDX30.Generator>()
.AddSingleton<IMetadataProvider, LocalMetadataProvider>()
.AddSingleton<IMetadataProvider, SbomApiMetadataProvider>()
.AddSingleton<IManifestInterface, SPDX22.Validator>()
.AddSingleton<IManifestInterface, SPDX30.Validator>()
.AddSingleton<IManifestConfigHandler, SPDX22ManifestConfigHandler>()
.AddSingleton<IManifestConfigHandler, SPDX30ManifestConfigHandler>())
.Build();
}

/// <inheritdoc/>
public override bool Execute()
{
try
{
var taskLoggingHelper = new TaskLoggingHelper(this);

// Validate required args and args that take paths as input.
if (!ValidateAndSanitizeRequiredParams() || !ValidateAndSanitizeNamespaceUriUniquePart())
{
return false;
}

var logVerbosity = ValidateAndAssignVerbosity();
var serilogLogVerbosity = MapSerilogLevel(logVerbosity);

var taskHost = Host.CreateDefaultBuilder()
.ConfigureServices((host, services) =>
services
.AddSbomTool(serilogLogVerbosity, taskLoggingHelper)
/* Manually adding some dependencies since `AddSbomTool()` does not add them when
* running the MSBuild Task from another project.
*/
.AddSingleton<ISourcesProvider, SbomPackagesProvider>()
.AddSingleton<ISourcesProvider, CGExternalDocumentReferenceProvider>()
.AddSingleton<ISourcesProvider, DirectoryTraversingFileToJsonProvider>()
.AddSingleton<ISourcesProvider, ExternalDocumentReferenceFileProvider>()
.AddSingleton<ISourcesProvider, ExternalDocumentReferenceProvider>()
.AddSingleton<ISourcesProvider, FileListBasedFileToJsonProvider>()
.AddSingleton<ISourcesProvider, SbomFileBasedFileToJsonProvider>()
.AddSingleton<ISourcesProvider, CGScannedExternalDocumentReferenceFileProvider>()
.AddSingleton<ISourcesProvider, CGScannedPackagesProvider>()
.AddSingleton<IAlgorithmNames, AlgorithmNames>()
.AddSingleton<IManifestGenerator, SPDX22.Generator>()
.AddSingleton<IManifestGenerator, SPDX30.Generator>()
.AddSingleton<IMetadataProvider, LocalMetadataProvider>()
.AddSingleton<IMetadataProvider, SbomApiMetadataProvider>()
.AddSingleton<IManifestInterface, SPDX22.Validator>()
.AddSingleton<IManifestInterface, SPDX30.Validator>()
.AddSingleton<IManifestConfigHandler, SPDX22ManifestConfigHandler>()
.AddSingleton<IManifestConfigHandler, SPDX30ManifestConfigHandler>())
.Build();

var generator = taskHost.Services.GetRequiredService<ISbomGenerator>();

// Set other configurations. The GenerateSBOMAsync() already sanitizes and checks for
@@ -91,7 +88,7 @@ public override bool Execute()
NamespaceUriBase = this.NamespaceBaseUri,
NamespaceUriUniquePart = this.NamespaceUriUniquePart,
DeleteManifestDirectoryIfPresent = this.DeleteManifestDirIfPresent,
Verbosity = ValidateAndAssignVerbosity(),
Verbosity = logVerbosity,
};
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
var result = System.Threading.Tasks.Task.Run(() => generator.GenerateSbomAsync(
@@ -113,6 +110,17 @@ public override bool Execute()
}
}

private LogEventLevel MapSerilogLevel(EventLevel logVerbosity) =>
logVerbosity switch
{
EventLevel.Critical => LogEventLevel.Fatal,
EventLevel.Error => LogEventLevel.Error,
EventLevel.Warning => LogEventLevel.Warning,
EventLevel.Informational => LogEventLevel.Information,
EventLevel.Verbose => LogEventLevel.Verbose,
_ => LogEventLevel.Information,
};

/// <summary>
/// Check for ManifestInfo and create an SbomSpecification accordingly.
/// </summary>
6 changes: 6 additions & 0 deletions src/Microsoft.Sbom.Targets/SbomInputValidator.cs
Original file line number Diff line number Diff line change
@@ -85,16 +85,22 @@ public EventLevel ValidateAndAssignVerbosity()
switch (this.Verbosity.ToLower().Trim())
{
case "verbose":
this.Verbosity = "Verbose";
return EventLevel.Verbose;
case "debug":
this.Verbosity = "Verbose";
return EventLevel.Verbose;
case "information":
this.Verbosity = "Information";
return EventLevel.Informational;
case "warning":
this.Verbosity = "Warning";
return EventLevel.Warning;
case "error":
this.Verbosity = "Error";
return EventLevel.Error;
case "fatal":
this.Verbosity = "Fatal";
return EventLevel.Critical;
default:
Log.LogWarning($"Unrecognized verbosity level specified. Setting verbosity level at {DefaultVerbosity}.");
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.IO;
#if NET472
using System.Linq;
#endif
using System.Reflection;
using System.Text.RegularExpressions;
using Microsoft.Build.Framework;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
@@ -33,6 +29,7 @@ public abstract class AbstractGenerateSbomTaskInputTests
private Mock<IBuildEngine> buildEngine;
private List<BuildErrorEventArgs> errors;
private List<BuildMessageEventArgs> messages;
private List<BuildWarningEventArgs> warnings;

[TestInitialize]
public void Startup()
@@ -41,12 +38,15 @@ public void Startup()
this.buildEngine = new Mock<IBuildEngine>();
this.errors = new List<BuildErrorEventArgs>();
this.messages = new List<BuildMessageEventArgs>();
this.warnings = new List<BuildWarningEventArgs>();
this.buildEngine.Setup(x => x.LogErrorEvent(It.IsAny<BuildErrorEventArgs>())).Callback<BuildErrorEventArgs>(e => errors.Add(e));
this.buildEngine.Setup(x => x.LogMessageEvent(It.IsAny<BuildMessageEventArgs>())).Callback<BuildMessageEventArgs>(msg => messages.Add(msg));
this.buildEngine.Setup(x => x.LogWarningEvent(It.IsAny<BuildWarningEventArgs>())).Callback<BuildWarningEventArgs>(w => warnings.Add(w));
}

[TestCleanup]
public void Cleanup() {
public void Cleanup()
{
// Clean up the manifest directory
if (Directory.Exists(DefaultManifestDirectory))
{
@@ -209,19 +209,30 @@ public void Sbom_Generation_Fails_For_Invalid_NamespaceUriUniquePart(string name
Assert.IsFalse(result);
}

internal class MSBuildMessageDebugView(BuildMessageEventArgs message)
{
public override string ToString()
{
if (message.Subcategory is not null && message.Code is not null)
{
return $"[{message.Timestamp}] [{message.Subcategory}/{message.Code}] {message.Message}";
}
else
{
return $"[{message.Timestamp}] {message.Message}";
}
}
}

/// <summary>
/// Test for ensuring GenerateSbom assigns a defualt Verbosity
/// Test for ensuring GenerateSbom assigns a default Verbosity
/// level when null input is provided.
/// </summary>
[TestMethod]
public void Sbom_Generation_Succeeds_For_Null_Verbosity()
{
// Arrange
// If Verbosity is null, the default value should be Information and is printed in the
// tool's standard output.
var pattern = new Regex("Verbosity=.*Value=Information");
var stringWriter = new StringWriter();
Console.SetOut(stringWriter);
// If Verbosity is null, the task should assign a default value should be 'Information'
var task = new GenerateSbom
{
BuildDropPath = CurrentDirectory,
@@ -239,15 +250,10 @@ public void Sbom_Generation_Succeeds_For_Null_Verbosity()

// Act
var result = task.Execute();
var output = stringWriter.ToString();

// Assert
Assert.IsTrue(result);
#if NET472
Assert.IsTrue(this.messages.Any(msg => pattern.IsMatch(msg.Message)));
#else
Assert.IsTrue(pattern.IsMatch(output));
#endif
Assert.AreEqual("Information", task.Verbosity);
}

/// <summary>
@@ -258,11 +264,7 @@ public void Sbom_Generation_Succeeds_For_Null_Verbosity()
public void Sbom_Generation_Succeeds_For_Invalid_Verbosity()
{
// Arrange
// If an invalid Verbosity is specified, the default value should be Information. It is also printed in the
// tool's standard output for the MSBuild Core task.
var pattern = new Regex("Verbosity=.*Value=Information");
var stringWriter = new StringWriter();
Console.SetOut(stringWriter);
// If an invalid Verbosity is specified, the default value should be Information.
var task = new GenerateSbom
{
BuildDropPath = CurrentDirectory,
@@ -280,18 +282,12 @@ public void Sbom_Generation_Succeeds_For_Invalid_Verbosity()

// Act
var result = task.Execute();
var output = stringWriter.ToString();

// Assert
Assert.IsTrue(result);
#if NET472
Assert.IsTrue(this.messages.Any(msg => pattern.IsMatch(msg.Message)));
#else
Assert.IsTrue(pattern.IsMatch(output));
#endif
Assert.AreEqual("Information", task.Verbosity);
}

#if !NET472
/// <summary>
/// Test to ensure GenerateSbom correctly parses and provides each EventLevel verbosity
/// values to the SBOM API.
@@ -311,47 +307,6 @@ public void Sbom_Generation_Assigns_Correct_Verbosity_IgnoreCase(string inputVer
}

// Arrange
var pattern = new Regex($"Verbosity=.*Value={mappedVerbosity}");
var stringWriter = new StringWriter();
Console.SetOut(stringWriter);
var task = new GenerateSbom
{
BuildDropPath = CurrentDirectory,
PackageSupplier = PackageSupplier,
PackageName = PackageName,
PackageVersion = PackageVersion,
NamespaceBaseUri = NamespaceBaseUri,
Verbosity = inputVerbosity,
ManifestInfo = this.SbomSpecification,
BuildEngine = this.buildEngine.Object,
};

// Act
var result = task.Execute();
var output = stringWriter.ToString();

// Assert
Assert.IsTrue(result, $"result: {result} is not set to true");
Assert.AreEqual(messageShouldBeLogged, pattern.IsMatch(output));
}
#else
/// <summary>
/// Test to ensure GenerateSbom correctly parses and provides each verbosity option
/// to the SBOM CLI.
/// </summary>
[TestMethod]
[DataRow("FATAL", "Fatal", false)]
[DataRow("information", "Information", true)]
[DataRow("vErBose", "Verbose", true)]
[DataRow("Warning", "Warning", false)]
[DataRow("eRRor", "Error", false)]
[DataRow("DeBug", "Debug", true)]
public void Sbom_Generation_Assigns_Correct_Verbosity_IgnoreCase(string inputVerbosity, string mappedVerbosity, bool messageShouldBeLogged)
{
// Arrange
var pattern = new Regex($"Verbosity=.*Value={mappedVerbosity}");
var stringWriter = new StringWriter();
Console.SetOut(stringWriter);
var task = new GenerateSbom
{
BuildDropPath = CurrentDirectory,
@@ -362,16 +317,16 @@ public void Sbom_Generation_Assigns_Correct_Verbosity_IgnoreCase(string inputVer
Verbosity = inputVerbosity,
ManifestInfo = this.SbomSpecification,
BuildEngine = this.buildEngine.Object,
#if NET472
SbomToolPath = SbomToolPath,
#endif
};

// Act
var result = task.Execute();
var output = stringWriter.ToString();

// Assert
Assert.IsTrue(result, $"result: {result} is not set to true");
Assert.AreEqual(messageShouldBeLogged, this.messages.Any(msg => pattern.IsMatch(msg.Message)));
Assert.IsTrue(result, $"result: {result} is not set to true.");
Assert.AreEqual(mappedVerbosity, task.Verbosity);
}
#endif
}