Skip to content
Open
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
68 changes: 68 additions & 0 deletions src/Build.UnitTests/BinaryLogger_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,74 @@ public void BinlogFileNameWildcardGeneration()
File.Create(_logFile).Dispose();
}

[Theory]
[InlineData("mylog.binlog", "mylog.binlog", BinaryLogger.ProjectImportsCollectionMode.Embed, false)]
[InlineData("LogFile=mylog.binlog", "mylog.binlog", BinaryLogger.ProjectImportsCollectionMode.Embed, false)]
[InlineData("\"mylog.binlog\"", "mylog.binlog", BinaryLogger.ProjectImportsCollectionMode.Embed, false)]
[InlineData("LogFile=\"mylog.binlog\"", "mylog.binlog", BinaryLogger.ProjectImportsCollectionMode.Embed, false)]
[InlineData("mylog.binlog;ProjectImports=None", "mylog.binlog", BinaryLogger.ProjectImportsCollectionMode.None, false)]
[InlineData("ProjectImports=None;mylog.binlog", "mylog.binlog", BinaryLogger.ProjectImportsCollectionMode.None, false)]
[InlineData("ProjectImports=Embed;mylog.binlog", "mylog.binlog", BinaryLogger.ProjectImportsCollectionMode.Embed, false)]
[InlineData("ProjectImports=ZipFile;mylog.binlog", "mylog.binlog", BinaryLogger.ProjectImportsCollectionMode.ZipFile, false)]
[InlineData("mylog.binlog;OmitInitialInfo", "mylog.binlog", BinaryLogger.ProjectImportsCollectionMode.Embed, true)]
[InlineData("OmitInitialInfo;mylog.binlog", "mylog.binlog", BinaryLogger.ProjectImportsCollectionMode.Embed, true)]
[InlineData("ProjectImports=None;OmitInitialInfo;mylog.binlog", "mylog.binlog", BinaryLogger.ProjectImportsCollectionMode.None, true)]
public void ParseParametersTests(string parametersString, string expectedLogFilePath, BinaryLogger.ProjectImportsCollectionMode expectedImportsMode, bool expectedOmitInitialInfo)
{
var result = BinaryLogger.ParseParameters(parametersString);

result.LogFilePath.ShouldBe(expectedLogFilePath);
result.ProjectImportsCollectionMode.ShouldBe(expectedImportsMode);
result.OmitInitialInfo.ShouldBe(expectedOmitInitialInfo);

// Create the expected log file to satisfy test environment expectations
File.Create(_logFile).Dispose();
Comment on lines +703 to +704
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test creates _logFile but uses expectedLogFilePath from test data. This creates a file with a different name than what's being tested. Either create a file matching expectedLogFilePath or remove this line if the test doesn't actually require a file to exist.

Copilot uses AI. Check for mistakes.
}

[Theory]
[InlineData("{}")] // Wildcard without extension
[InlineData("{}.binlog")] // Wildcard with extension
[InlineData("mylog-{}.binlog")] // Wildcard with prefix
[InlineData("LogFile={}.binlog")] // Wildcard with LogFile= prefix
public void ParseParameters_WildcardPath_ReturnsNullPath(string parametersString)
{
using (TestEnvironment env = TestEnvironment.Create())
{
// Enable Wave17_12 to support wildcard parameters
ChangeWaves.ResetStateForTests();
env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", "");
BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();

var result = BinaryLogger.ParseParameters(parametersString);

result.LogFilePath.ShouldBeNull();
}

// Create the expected log file to satisfy test environment expectations
File.Create(_logFile).Dispose();
Comment on lines +726 to +727
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests are testing parameter parsing which should not require a file to exist. Creating _logFile in these tests appears unnecessary and may indicate copy-paste from other tests. Consider removing these file creation calls unless they're truly required by the test infrastructure.

Copilot uses AI. Check for mistakes.
}

[Fact]
public void ParseParameters_NullParameter_ThrowsLoggerException()
{
Should.Throw<LoggerException>(() => BinaryLogger.ParseParameters(null));

// Create the expected log file to satisfy test environment expectations
File.Create(_logFile).Dispose();
Comment on lines +735 to +736
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests are testing parameter parsing which should not require a file to exist. Creating _logFile in these tests appears unnecessary and may indicate copy-paste from other tests. Consider removing these file creation calls unless they're truly required by the test infrastructure.

Copilot uses AI. Check for mistakes.
}

[Theory]
[InlineData("invalidparameter")]
[InlineData("mylog.txt")] // Wrong extension
[InlineData("LogFile=mylog.txt")] // Wrong extension with LogFile prefix
public void ParseParameters_InvalidParameter_ThrowsLoggerException(string parametersString)
{
Should.Throw<LoggerException>(() => BinaryLogger.ParseParameters(parametersString));

// Create the expected log file to satisfy test environment expectations
File.Create(_logFile).Dispose();
Comment on lines +747 to +748
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests are testing parameter parsing which should not require a file to exist. Creating _logFile in these tests appears unnecessary and may indicate copy-paste from other tests. Consider removing these file creation calls unless they're truly required by the test infrastructure.

Copilot uses AI. Check for mistakes.
}

public void Dispose()
{
_env.Dispose();
Expand Down
229 changes: 198 additions & 31 deletions src/Build/Logging/BinaryLogger/BinaryLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,32 @@

namespace Microsoft.Build.Logging
{
/// <summary>
/// Represents the parsed parameters for a BinaryLogger.
/// </summary>
public sealed class BinaryLoggerParameters
{
/// <summary>
/// Gets the log file path. May be null if not specified (defaults to "msbuild.binlog").
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation comment is misleading. The property returns null for wildcard paths or when not specified, but the default of 'msbuild.binlog' is only applied later in ProcessParameters. Consider: 'Gets the log file path. Returns null if not specified or if the path contains wildcards.'

Suggested change
/// Gets the log file path. May be null if not specified (defaults to "msbuild.binlog").
/// Gets the log file path. Returns null if not specified or if the path contains wildcards.

Copilot uses AI. Check for mistakes.
/// </summary>
public string LogFilePath { get; internal set; }

/// <summary>
/// Gets the project imports collection mode.
/// </summary>
public BinaryLogger.ProjectImportsCollectionMode ProjectImportsCollectionMode { get; internal set; } = BinaryLogger.ProjectImportsCollectionMode.Embed;

/// <summary>
/// Gets whether the ProjectImports parameter was explicitly specified in the parameters string.
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation should clarify why this property is needed. Consider adding: 'This is used to preserve manually set CollectProjectImports values when Initialize() is called without an explicit ProjectImports parameter.'

Suggested change
/// Gets whether the ProjectImports parameter was explicitly specified in the parameters string.
/// Gets whether the ProjectImports parameter was explicitly specified in the parameters string.
/// This is used to preserve manually set CollectProjectImports values when Initialize() is called
/// without an explicit ProjectImports parameter.

Copilot uses AI. Check for mistakes.
/// </summary>
internal bool HasProjectImportsParameter { get; set; }

/// <summary>
/// Gets whether to omit initial info from the log.
/// </summary>
public bool OmitInitialInfo { get; internal set; }
}

/// <summary>
/// A logger that serializes all incoming BuildEventArgs in a compressed binary file (*.binlog). The file
/// can later be played back and piped into other loggers (file, console, etc) to reconstruct the log contents
Expand Down Expand Up @@ -100,6 +126,14 @@ public sealed class BinaryLogger : ILogger
// skip them if they are not known to it. Example of change requiring the increment would be the introduction of strings deduplication)
internal const int MinimumReaderVersion = 18;

// Parameter name constants
private const string LogFileParameterPrefix = "LogFile=";
private const string BinlogFileExtension = ".binlog";
private const string OmitInitialInfoParameter = "OmitInitialInfo";
private const string ProjectImportsNoneParameter = "ProjectImports=None";
private const string ProjectImportsEmbedParameter = "ProjectImports=Embed";
private const string ProjectImportsZipFileParameter = "ProjectImports=ZipFile";

private Stream stream;
private BinaryWriter binaryWriter;
private BuildEventArgsWriter eventArgsWriter;
Expand Down Expand Up @@ -130,6 +164,144 @@ public enum ProjectImportsCollectionMode
ZipFile,
}

/// <summary>
/// Parses the parameters string for a BinaryLogger.
/// </summary>
/// <param name="parametersString">The parameters string to parse (e.g., "LogFile=msbuild.binlog;ProjectImports=None").</param>
/// <returns>A <see cref="BinaryLoggerParameters"/> object containing the parsed parameters.</returns>
/// <exception cref="LoggerException">Thrown when the parameters string contains invalid parameters.</exception>
/// <remarks>
/// This method parses the semicolon-delimited parameters string used by the BinaryLogger.
/// Supported parameters include:
/// - LogFile=&lt;path&gt; or just &lt;path&gt; (must end with .binlog): specifies the output file path
/// - ProjectImports=None|Embed|ZipFile: controls project imports collection
/// - OmitInitialInfo: omits initial build information
///
/// Wildcards ({}) in the LogFile path are NOT expanded by this method. The returned LogFilePath
/// will be null for wildcard patterns, and callers should handle expansion separately if needed.
/// </remarks>
public static BinaryLoggerParameters ParseParameters(string parametersString)
{
if (parametersString == null)
{
throw new LoggerException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("InvalidBinaryLoggerParameters", ""));
Comment on lines +185 to +187
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exception documentation in the method summary states 'Thrown when the parameters string contains invalid parameters' but this throws for null input. Consider updating the exception documentation to: 'Thrown when the parameters string is null or contains invalid parameters.'

Copilot uses AI. Check for mistakes.
}

var result = new BinaryLoggerParameters();
var parameters = parametersString.Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries);

foreach (var parameter in parameters)
{
if (TryParseProjectImports(parameter, result))
{
continue;
}

if (string.Equals(parameter, OmitInitialInfoParameter, StringComparison.OrdinalIgnoreCase))
{
result.OmitInitialInfo = true;
continue;
}

if (TryParsePathParameter(parameter, out string filePath))
{
result.LogFilePath = filePath;
continue;
}

throw new LoggerException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("InvalidBinaryLoggerParameters", parameter));
}

return result;
}

/// <summary>
/// Attempts to parse a ProjectImports parameter.
/// </summary>
/// <param name="parameter">The parameter to parse.</param>
/// <param name="result">The BinaryLoggerParameters object to update.</param>
/// <returns>True if the parameter was a ProjectImports parameter; otherwise, false.</returns>
private static bool TryParseProjectImports(string parameter, BinaryLoggerParameters result)
{
if (TrySetProjectImportsMode(parameter, ProjectImportsNoneParameter, ProjectImportsCollectionMode.None, result))
{
return true;
}

if (TrySetProjectImportsMode(parameter, ProjectImportsEmbedParameter, ProjectImportsCollectionMode.Embed, result))
{
return true;
}

if (TrySetProjectImportsMode(parameter, ProjectImportsZipFileParameter, ProjectImportsCollectionMode.ZipFile, result))
{
return true;
}

return false;
}

/// <summary>
/// Attempts to match and set a ProjectImports mode.
/// </summary>
/// <param name="parameter">The parameter to check.</param>
/// <param name="expectedParameter">The expected parameter string.</param>
/// <param name="mode">The mode to set if matched.</param>
/// <param name="result">The BinaryLoggerParameters object to update.</param>
/// <returns>True if the parameter matched; otherwise, false.</returns>
private static bool TrySetProjectImportsMode(string parameter, string expectedParameter, ProjectImportsCollectionMode mode, BinaryLoggerParameters result)
{
if (string.Equals(parameter, expectedParameter, StringComparison.OrdinalIgnoreCase))
{
result.ProjectImportsCollectionMode = mode;
result.HasProjectImportsParameter = true;
return true;
}

return false;
}

/// <summary>
/// Attempts to parse a file path parameter from a BinaryLogger parameter string.
/// </summary>
/// <param name="parameter">The parameter to parse.</param>
/// <param name="filePath">The parsed file path, or null if the parameter contains wildcards.</param>
/// <returns>True if the parameter is a valid file path parameter; otherwise, false.</returns>
/// <remarks>
/// This method recognizes file paths in the following formats:
/// - "LogFile=&lt;path&gt;"
/// - "&lt;path&gt;" (must end with .binlog)
///
/// If the path contains wildcards ({}), the method returns true but sets filePath to null,
/// as wildcard expansion requires runtime context.
/// </remarks>
private static bool TryParsePathParameter(string parameter, out string filePath)
{
bool hasPathPrefix = parameter.StartsWith(LogFileParameterPrefix, StringComparison.OrdinalIgnoreCase);

if (hasPathPrefix)
{
parameter = parameter.Substring(LogFileParameterPrefix.Length);
}

parameter = parameter.Trim('"');

bool isWildcard = ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_12) && parameter.Contains("{}");
bool hasProperExtension = parameter.EndsWith(BinlogFileExtension, StringComparison.OrdinalIgnoreCase);

filePath = parameter;

if (isWildcard)
{
// For wildcards, we return true to indicate this is a valid path parameter,
// but set filePath to null since we can't expand it without instance context
filePath = null;
return true;
}

return hasProperExtension;
}

/// <summary>
/// Gets or sets whether to capture and embed project and target source files used during the build.
/// </summary>
Expand Down Expand Up @@ -426,40 +598,35 @@ private void CollectImports(BuildEventArgs e)
/// </exception>
private void ProcessParameters(out bool omitInitialInfo)
{
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling ParseParameters(Parameters) will throw LoggerException if Parameters is null, but the original implementation allowed null Parameters and would throw with a specific error. This changes the error handling behavior. Consider handling null Parameters before calling ParseParameters or documenting this breaking change.

Suggested change
{
{
if (Parameters == null)
{
string errorCode;
string helpKeyword;
string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(
out errorCode,
out helpKeyword,
"LoggerParametersNull");
throw new LoggerException(message, errorCode, helpKeyword);
}

Copilot uses AI. Check for mistakes.
if (Parameters == null)
var parsedParams = ParseParameters(Parameters);

omitInitialInfo = parsedParams.OmitInitialInfo;

// Only set CollectProjectImports if it was explicitly specified in parameters
if (parsedParams.HasProjectImportsParameter)
{
throw new LoggerException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("InvalidBinaryLoggerParameters", ""));
CollectProjectImports = parsedParams.ProjectImportsCollectionMode;
}

omitInitialInfo = false;
var parameters = Parameters.Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries);
foreach (var parameter in parameters)
// Handle the file path - expand wildcards if needed
if (parsedParams.LogFilePath == null)
{
if (string.Equals(parameter, "ProjectImports=None", StringComparison.OrdinalIgnoreCase))
{
CollectProjectImports = ProjectImportsCollectionMode.None;
}
else if (string.Equals(parameter, "ProjectImports=Embed", StringComparison.OrdinalIgnoreCase))
// Either no path was specified, or it contained wildcards
// Check if any parameter was a wildcard path
var parameters = Parameters.Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries);
foreach (var parameter in parameters)
{
CollectProjectImports = ProjectImportsCollectionMode.Embed;
}
else if (string.Equals(parameter, "ProjectImports=ZipFile", StringComparison.OrdinalIgnoreCase))
{
CollectProjectImports = ProjectImportsCollectionMode.ZipFile;
}
else if (string.Equals(parameter, "OmitInitialInfo", StringComparison.OrdinalIgnoreCase))
{
omitInitialInfo = true;
}
else if (TryInterpretPathParameter(parameter, out string filePath))
{
FilePath = filePath;
}
else
{
throw new LoggerException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("InvalidBinaryLoggerParameters", parameter));
if (TryInterpretPathParameter(parameter, out string filePath))
{
FilePath = filePath;
break;
}
}
Comment on lines +616 to 624
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code re-parses the Parameters string even though we just parsed it in ParseParameters. This is inefficient and creates duplicate parsing logic. Consider having ParseParameters track whether it found a wildcard and store that information, or return all path parameters found (including wildcards) so we don't need to re-parse.

Copilot uses AI. Check for mistakes.
}
else
{
FilePath = parsedParams.LogFilePath;
}

if (FilePath == null)
{
Expand All @@ -482,17 +649,17 @@ private void ProcessParameters(out bool omitInitialInfo)

private bool TryInterpretPathParameter(string parameter, out string filePath)
{
bool hasPathPrefix = parameter.StartsWith("LogFile=", StringComparison.OrdinalIgnoreCase);
bool hasPathPrefix = parameter.StartsWith(LogFileParameterPrefix, StringComparison.OrdinalIgnoreCase);

if (hasPathPrefix)
{
parameter = parameter.Substring("LogFile=".Length);
parameter = parameter.Substring(LogFileParameterPrefix.Length);
}

parameter = parameter.Trim('"');

bool isWildcard = ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_12) && parameter.Contains("{}");
bool hasProperExtension = parameter.EndsWith(".binlog", StringComparison.OrdinalIgnoreCase);
bool hasProperExtension = parameter.EndsWith(BinlogFileExtension, StringComparison.OrdinalIgnoreCase);
filePath = parameter;

if (!isWildcard)
Expand All @@ -504,7 +671,7 @@ private bool TryInterpretPathParameter(string parameter, out string filePath)

if (!hasProperExtension)
{
filePath += ".binlog";
filePath += BinlogFileExtension;
}
return true;
}
Expand Down