|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
#if MICROSOFT_BUILD_ENGINE_UNITTESTS
using System.Text;
using Microsoft.Build.BackEnd.Logging;
#endif
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Logging;
using Microsoft.Build.Shared;
using Shouldly;
#nullable disable
namespace Microsoft.Build.UnitTests
{
public partial class TestEnvironment
{
// reset the default build manager and the state it might have accumulated from other tests
#pragma warning disable CA1823 // Avoid unused private fields
private object _resetBuildManager = new ResetDefaultBuildManager();
#pragma warning restore CA1823 // Avoid unused private fields
private sealed class ResetDefaultBuildManager
{
public static FieldInfo SingletonField;
public ResetDefaultBuildManager()
{
if (DefaultBuildManagerIsInstantiated())
{
DisposeDefaultBuildManager();
}
}
private static void DisposeDefaultBuildManager()
{
try
{
BuildManager.DefaultBuildManager.BeginBuild(
new BuildParameters()
{
EnableNodeReuse = false,
ShutdownInProcNodeOnBuildFinish = true
});
}
finally
{
BuildManager.DefaultBuildManager.EndBuild();
BuildManager.DefaultBuildManager.Dispose();
}
}
private static bool DefaultBuildManagerIsInstantiated()
{
if (SingletonField == null)
{
SingletonField = typeof(BuildManager).GetField("s_singletonInstance", BindingFlags.Static | BindingFlags.NonPublic);
}
SingletonField.ShouldNotBeNull();
return SingletonField.GetValue(null) != null;
}
}
/// <summary>
/// Creates a test variant that corresponds to a project collection which will have its projects unloaded,
/// loggers unregistered, toolsets removed and disposed when the test completes
/// </summary>
/// <returns></returns>
public TransientProjectCollection CreateProjectCollection()
{
return WithTransientTestState(new TransientProjectCollection());
}
/// <summary>
/// Creates a test variant representing a test project with files relative to the project root. All files
/// and the root will be cleaned up when the test completes.
/// </summary>
/// <param name="projectFileName">Name of the project file with extension to be created.</param>
/// <param name="projectContents">Contents of the project file to be created.</param>
/// <param name="files">Files to be created.</param>
/// <param name="relativePathFromRootToProject">Path for the specified files to be created in relative to
/// the root of the project directory.</param>
public TransientTestProjectWithFiles CreateTestProjectWithFiles(string projectFileName, [StringSyntax(StringSyntaxAttribute.Xml)] string projectContents, string[] files = null, string relativePathFromRootToProject = ".")
=> WithTransientTestState(new TransientTestProjectWithFiles(projectFileName, projectContents, files, relativePathFromRootToProject));
/// <summary>
/// Creates a test variant representing a test project with files relative to the project root. All files
/// and the root will be cleaned up when the test completes.
/// </summary>
/// <param name="projectContents">Contents of the project file to be created.</param>
/// <param name="files">Files to be created.</param>
/// <param name="relativePathFromRootToProject">Path for the specified files to be created in relative to
/// the root of the project directory.</param>
public TransientTestProjectWithFiles CreateTestProjectWithFiles([StringSyntax(StringSyntaxAttribute.Xml)] string projectContents, string[] files = null, string relativePathFromRootToProject = ".")
=> CreateTestProjectWithFiles("build.proj", projectContents, files, relativePathFromRootToProject);
}
public class TransientTestProjectWithFiles : TransientTestState
{
private readonly TransientTestFolder _folder;
public string TestRoot => _folder.Path;
public string[] CreatedFiles { get; }
public string ProjectFile { get; }
public TransientTestProjectWithFiles(
string projectFileName,
[StringSyntax(StringSyntaxAttribute.Xml)] string projectContents,
string[] files,
string relativePathFromRootToProject = ".")
{
_folder = new TransientTestFolder();
var projectDir = Path.GetFullPath(Path.Combine(TestRoot, relativePathFromRootToProject));
Directory.CreateDirectory(projectDir);
ProjectFile = Path.GetFullPath(Path.Combine(projectDir, projectFileName));
File.WriteAllText(ProjectFile, ObjectModelHelpers.CleanupFileContents(projectContents));
CreatedFiles = Helpers.CreateFilesInDirectory(TestRoot, files);
}
public MockLogger BuildProjectExpectFailure(IDictionary<string, string> globalProperties = null, string toolsVersion = null, bool validateLoggerRoundtrip = true)
{
BuildProject(globalProperties, toolsVersion, out MockLogger logger, validateLoggerRoundtrip).ShouldBeFalse();
return logger;
}
public MockLogger BuildProjectExpectSuccess(IDictionary<string, string> globalProperties = null, string toolsVersion = null, bool validateLoggerRoundtrip = true)
{
BuildProject(globalProperties, toolsVersion, out MockLogger logger, validateLoggerRoundtrip).ShouldBeTrue();
return logger;
}
public override void Revert()
{
_folder.Revert();
}
private IEnumerable<(ILogger logger, Func<string> textGetter)> GetLoggers()
{
var result = new List<(ILogger logger, Func<string> textGetter)>();
// Add binlogger first - so that it get's all messages (the logger initialization messages goes only to so far initialized loggers)
result.Add(GetBinaryLogger());
result.Add(GetMockLogger());
#if MICROSOFT_BUILD_ENGINE_UNITTESTS
result.Add(GetSerialLogger());
result.Add(GetParallelLogger());
#endif
return result;
}
private (ILogger logger, Func<string> textGetter) GetMockLogger()
{
var logger = new MockLogger();
return (logger, () => logger.FullLog);
}
#if MICROSOFT_BUILD_ENGINE_UNITTESTS
private (ILogger, Func<string>) GetSerialLogger()
{
var sb = new StringBuilder();
var serialFromBuild = new SerialConsoleLogger(LoggerVerbosity.Diagnostic, t => sb.Append(t), colorSet: null, colorReset: null);
serialFromBuild.Parameters = "NOPERFORMANCESUMMARY";
return (serialFromBuild, () => sb.ToString());
}
private (ILogger, Func<string>) GetParallelLogger()
{
var sb = new StringBuilder();
var parallelFromBuild = new ParallelConsoleLogger(LoggerVerbosity.Diagnostic, t => sb.Append(t), colorSet: null, colorReset: null);
parallelFromBuild.Parameters = "NOPERFORMANCESUMMARY";
return (parallelFromBuild, () => sb.ToString());
}
#endif
private (ILogger, Func<string>) GetBinaryLogger()
{
var binaryLogger = new BinaryLogger();
string binaryLoggerFilePath = Path.GetFullPath(Path.Combine(TestRoot, Guid.NewGuid().ToString() + ".binlog"));
binaryLogger.CollectProjectImports = BinaryLogger.ProjectImportsCollectionMode.None;
binaryLogger.Parameters = binaryLoggerFilePath;
return (binaryLogger, null);
}
private bool BuildProject(
IDictionary<string, string> globalProperties,
string toolsVersion,
out MockLogger mockLogger,
bool validateLoggerRoundtrip = true)
{
var expectedLoggerPairs = validateLoggerRoundtrip ? GetLoggers() : new[] { GetMockLogger() };
var expectedLoggers = expectedLoggerPairs.Select(l => l.logger).ToArray();
mockLogger = expectedLoggers.OfType<MockLogger>().First();
var binaryLogger = expectedLoggers.OfType<BinaryLogger>().FirstOrDefault();
try
{
using (ProjectCollection projectCollection = new ProjectCollection())
{
Project project = new Project(ProjectFile, globalProperties, toolsVersion, projectCollection);
return project.Build(expectedLoggers);
}
}
finally
{
if (binaryLogger != null)
{
string binaryLoggerFilePath = binaryLogger.Parameters;
var actualLoggerPairs = GetLoggers().Where(l => l.logger is not BinaryLogger).ToArray();
expectedLoggerPairs = expectedLoggerPairs.Where(l => l.logger is not BinaryLogger).ToArray();
PlaybackBinlog(binaryLoggerFilePath, actualLoggerPairs.Select(k => k.logger).ToArray());
FileUtilities.DeleteNoThrow(binaryLoggerFilePath);
var pairs = expectedLoggerPairs.Zip(actualLoggerPairs, (expected, actual) => (expected, actual));
foreach (var pair in pairs)
{
var expectedText = pair.expected.textGetter();
var actualText = pair.actual.textGetter();
actualText.ShouldContainWithoutWhitespace(expectedText);
}
}
}
}
private static void PlaybackBinlog(string binlogFilePath, params ILogger[] loggers)
{
var replayEventSource = new BinaryLogReplayEventSource();
foreach (var logger in loggers)
{
if (logger is INodeLogger nodeLogger)
{
nodeLogger.Initialize(replayEventSource, 1);
}
else
{
logger.Initialize(replayEventSource);
}
}
replayEventSource.Replay(binlogFilePath);
foreach (var logger in loggers)
{
logger.Shutdown();
}
}
}
public class TransientProjectCollection : TransientTestState
{
public ProjectCollection Collection { get; }
public TransientProjectCollection()
{
Collection = new ProjectCollection();
}
public override void Revert()
{
Collection.UnloadAllProjects();
Collection.UnregisterAllLoggers();
Collection.RemoveAllToolsets();
Collection.Dispose();
}
}
}
|