From c80ee2100b25700405c6df49e8128a27b1afc660 Mon Sep 17 00:00:00 2001 From: Dave Glick Date: Tue, 2 Oct 2018 10:59:59 -0400 Subject: [PATCH] Adds support for reading binary log files --- .../AnalyzerResultExtensions.cs | 11 +++--- src/Buildalyzer/AnalyzerManager.cs | 29 ++++++++++++++ src/Buildalyzer/AnalyzerResult.cs | 23 +++++++++-- src/Buildalyzer/AnalyzerResults.cs | 4 +- src/Buildalyzer/Buildalyzer.csproj | 1 + .../Logging/BuildEventArgsReaderProxy.cs | 36 +++++++++++++++++ src/Buildalyzer/Logging/EventProcessor.cs | 33 ++++++++++------ src/Buildalyzer/ProjectAnalyzer.cs | 13 +++---- .../Integration/OpenSourceProjectsFixture.cs | 3 +- .../Integration/SimpleProjectsFixture.cs | 39 +++++++++++++++++++ 10 files changed, 161 insertions(+), 31 deletions(-) create mode 100644 src/Buildalyzer/Logging/BuildEventArgsReaderProxy.cs diff --git a/src/Buildalyzer.Workspaces/AnalyzerResultExtensions.cs b/src/Buildalyzer.Workspaces/AnalyzerResultExtensions.cs index 1f4115bb..ed0d0ebb 100644 --- a/src/Buildalyzer.Workspaces/AnalyzerResultExtensions.cs +++ b/src/Buildalyzer.Workspaces/AnalyzerResultExtensions.cs @@ -72,7 +72,7 @@ public static Project AddToWorkspace(this AnalyzerResult analyzerResult, Workspa { if(!existingProject.Id.Equals(projectId) && _projectReferences.TryGetValue(existingProject.Id, out string[] existingReferences) - && existingReferences.Contains(analyzerResult.Analyzer.ProjectFile.Path)) + && existingReferences.Contains(analyzerResult.ProjectFilePath)) { // Add the reference to the existing project ProjectReference projectReference = new ProjectReference(projectId); @@ -105,15 +105,15 @@ public static Project AddToWorkspace(this AnalyzerResult analyzerResult, Workspa private static ProjectInfo GetProjectInfo(AnalyzerResult analyzerResult, Workspace workspace, ProjectId projectId) { - string projectName = Path.GetFileNameWithoutExtension(analyzerResult.Analyzer.ProjectFile.Path); - string languageName = GetLanguageName(analyzerResult.Analyzer.ProjectFile.Path); + string projectName = Path.GetFileNameWithoutExtension(analyzerResult.ProjectFilePath); + string languageName = GetLanguageName(analyzerResult.ProjectFilePath); ProjectInfo projectInfo = ProjectInfo.Create( projectId, VersionStamp.Create(), projectName, projectName, languageName, - filePath: analyzerResult.Analyzer.ProjectFile.Path, + filePath: analyzerResult.ProjectFilePath, outputFilePath: analyzerResult.GetProperty("TargetPath"), documents: GetDocuments(analyzerResult, projectId), projectReferences: GetExistingProjectReferences(analyzerResult, workspace), @@ -166,9 +166,10 @@ private static IEnumerable GetExistingProjectReferences(Analyz private static IEnumerable GetReferencedAnalyzerProjects(AnalyzerResult analyzerResult) => analyzerResult.ProjectReferences - .Select(x => analyzerResult.Analyzer.Manager.Projects.TryGetValue(x, out ProjectAnalyzer a) ? a : null) + .Select(x => analyzerResult.Manager.Projects.TryGetValue(x, out ProjectAnalyzer a) ? a : null) .Where(x => x != null) ?? Array.Empty(); + private static IEnumerable GetDocuments(AnalyzerResult analyzerResult, ProjectId projectId) => analyzerResult .SourceFiles ?.Where(File.Exists) diff --git a/src/Buildalyzer/AnalyzerManager.cs b/src/Buildalyzer/AnalyzerManager.cs index eebc78ca..d79596b7 100644 --- a/src/Buildalyzer/AnalyzerManager.cs +++ b/src/Buildalyzer/AnalyzerManager.cs @@ -2,13 +2,17 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.IO.Compression; using System.Linq; using System.Xml.Linq; using Buildalyzer.Construction; using Buildalyzer.Environment; +using Buildalyzer.Logging; using Microsoft.Build.Construction; using Microsoft.Build.Execution; using Microsoft.Build.Framework; +using Microsoft.Build.Logging; +using Microsoft.Build.Logging.StructuredLogger; using Microsoft.Extensions.Logging; namespace Buildalyzer @@ -83,6 +87,31 @@ public void SetEnvironmentVariable(string key, string value) public ProjectAnalyzer GetProject(string projectFilePath) => GetProject(projectFilePath, null); + /// + /// Analyzes an MSBuild binary log file. + /// + /// The path to the binary log file. + /// MSBuild loggers to replay events from the log to. + /// A dictionary of target frameworks to . + public AnalyzerResults Analyze(string binLogPath, IEnumerable buildLoggers = null) + { + binLogPath = NormalizePath(binLogPath); + if(!File.Exists(binLogPath)) + { + throw new ArgumentException($"The path {binLogPath} could not be found."); + } + + BinLogReader reader = new BinLogReader(); + using (EventProcessor eventProcessor = new EventProcessor(this, null, buildLoggers, reader, true)) + { + reader.Replay(binLogPath); + return new AnalyzerResults + { + { eventProcessor.Results, eventProcessor.OverallSuccess } + }; + } + } + private ProjectAnalyzer GetProject(string projectFilePath, ProjectInSolution projectInSolution) { if (projectFilePath == null) diff --git a/src/Buildalyzer/AnalyzerResult.cs b/src/Buildalyzer/AnalyzerResult.cs index 0dcb7d0c..71910753 100644 --- a/src/Buildalyzer/AnalyzerResult.cs +++ b/src/Buildalyzer/AnalyzerResult.cs @@ -19,17 +19,32 @@ public class AnalyzerResult private readonly Guid _projectGuid; private List<(string, string)> _cscCommandLineArguments; - internal AnalyzerResult(ProjectAnalyzer analyzer) + internal AnalyzerResult(string projectFilePath, AnalyzerManager manager, ProjectAnalyzer analyzer) { + ProjectFilePath = projectFilePath; + Manager = manager; Analyzer = analyzer; string projectGuid = GetProperty(nameof(ProjectGuid)); if(string.IsNullOrEmpty(projectGuid) || !Guid.TryParse(projectGuid, out _projectGuid)) { - _projectGuid = analyzer.ProjectGuid; + _projectGuid = analyzer == null + ? GuidUtility.Create(GuidUtility.UrlNamespace, ProjectFilePath) + : analyzer.ProjectGuid; } } + /// + /// The full normalized path to the project file. + /// + public string ProjectFilePath { get; } + + public AnalyzerManager Manager { get; } + + /// + /// Gets the that generated this result + /// or null if the result came from a binary log file. + /// public ProjectAnalyzer Analyzer { get; } public bool Succeeded { get; internal set; } @@ -67,7 +82,7 @@ public string GetProperty(string name) => ?.Where(x => x.Item1 == null && !string.Equals(Path.GetFileName(x.Item2), "csc.dll", StringComparison.OrdinalIgnoreCase) && !string.Equals(Path.GetFileName(x.Item2), "csc.exe", StringComparison.OrdinalIgnoreCase)) - .Select(x => AnalyzerManager.NormalizePath(Path.Combine(Path.GetDirectoryName(Analyzer.ProjectFile.Path), x.Item2))) + .Select(x => AnalyzerManager.NormalizePath(Path.Combine(Path.GetDirectoryName(ProjectFilePath), x.Item2))) .ToArray() ?? Array.Empty(); public string[] References => @@ -79,7 +94,7 @@ public string GetProperty(string name) => public IEnumerable ProjectReferences => Items.TryGetValue("ProjectReference", out ProjectItem[] items) ? items.Select(x => AnalyzerManager.NormalizePath( - Path.Combine(Path.GetDirectoryName(Analyzer.ProjectFile.Path), x.ItemSpec))) + Path.Combine(Path.GetDirectoryName(ProjectFilePath), x.ItemSpec))) : Array.Empty(); internal void ProcessProject(ProjectStartedEventArgs e) diff --git a/src/Buildalyzer/AnalyzerResults.cs b/src/Buildalyzer/AnalyzerResults.cs index a31e5f33..318be24e 100644 --- a/src/Buildalyzer/AnalyzerResults.cs +++ b/src/Buildalyzer/AnalyzerResults.cs @@ -10,9 +10,7 @@ public class AnalyzerResults : IEnumerable private bool? _overallSuccess = null; public bool OverallSuccess => _overallSuccess.HasValue ? _overallSuccess.Value : false; - - internal void Add(AnalyzerResult result) => _results.Add(result.TargetFramework ?? string.Empty, result); - + internal void Add(IEnumerable results, bool overallSuccess) { foreach (AnalyzerResult result in results) diff --git a/src/Buildalyzer/Buildalyzer.csproj b/src/Buildalyzer/Buildalyzer.csproj index db8fbf0d..edadd42e 100644 --- a/src/Buildalyzer/Buildalyzer.csproj +++ b/src/Buildalyzer/Buildalyzer.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Buildalyzer/Logging/BuildEventArgsReaderProxy.cs b/src/Buildalyzer/Logging/BuildEventArgsReaderProxy.cs new file mode 100644 index 00000000..9b6f1301 --- /dev/null +++ b/src/Buildalyzer/Logging/BuildEventArgsReaderProxy.cs @@ -0,0 +1,36 @@ +using Microsoft.Build.Framework; +using Microsoft.Build.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; + +namespace Buildalyzer.Logging +{ + internal class BuildEventArgsReaderProxy + { + private readonly Func _read; + + public BuildEventArgsReaderProxy(BinaryReader reader) + { + // Use reflection to get the Microsoft.Build.Logging.BuildEventArgsReader.Read() method + object argsReader; + Type buildEventArgsReader = typeof(BinaryLogger).GetTypeInfo().Assembly.GetType("Microsoft.Build.Logging.BuildEventArgsReader"); + ConstructorInfo readerCtor = buildEventArgsReader.GetConstructor(new[] { typeof(BinaryReader) }); + if (readerCtor != null) + { + argsReader = readerCtor.Invoke(new[] { reader }); + } + else + { + readerCtor = buildEventArgsReader.GetConstructor(new[] { typeof(BinaryReader), typeof(int) }); + argsReader = readerCtor.Invoke(new object[] { reader, 7 }); + } + MethodInfo readMethod = buildEventArgsReader.GetMethod("Read"); + _read = (Func)readMethod.CreateDelegate(typeof(Func), argsReader); + } + + public BuildEventArgs Read() => _read(); + } +} diff --git a/src/Buildalyzer/Logging/EventProcessor.cs b/src/Buildalyzer/Logging/EventProcessor.cs index 57eb3280..f0148230 100644 --- a/src/Buildalyzer/Logging/EventProcessor.cs +++ b/src/Buildalyzer/Logging/EventProcessor.cs @@ -11,29 +11,34 @@ internal class EventProcessor : IDisposable { private readonly Dictionary _results = new Dictionary(); private readonly Stack _currentResult = new Stack(); + private readonly AnalyzerManager _manager; private readonly ProjectAnalyzer _analyzer; private readonly ILogger _logger; - private readonly IEnumerable _loggers; + private readonly IEnumerable _buildLoggers; private readonly IEventSource _eventSource; private readonly bool _analyze; + private string _projectFilePath; + public bool OverallSuccess { get; private set; } public IEnumerable Results => _results.Values; - public EventProcessor(ProjectAnalyzer analyzer, IEnumerable loggers, IEventSource eventSource, bool analyze) + public EventProcessor(AnalyzerManager manager, ProjectAnalyzer analyzer, IEnumerable buildLoggers, IEventSource eventSource, bool analyze) { + _manager = manager; _analyzer = analyzer; - _logger = analyzer.Manager.LoggerFactory?.CreateLogger(); - _loggers = loggers; + _logger = manager.LoggerFactory?.CreateLogger(); + _buildLoggers = buildLoggers ?? Array.Empty(); _eventSource = eventSource; _analyze = analyze; + _projectFilePath = _analyzer?.ProjectFile.Path; + // Initialize the loggers - // TODO: Figure out what to do with loggers: don't filter if using loggers, what about console (use stdout?) - foreach (Microsoft.Build.Framework.ILogger logger in loggers) + foreach (Microsoft.Build.Framework.ILogger buildLogger in _buildLoggers) { - logger.Initialize(eventSource); + buildLogger.Initialize(eventSource); } // Send events to the tree constructor @@ -52,8 +57,14 @@ public EventProcessor(ProjectAnalyzer analyzer, IEnumerable() @@ -62,7 +73,7 @@ private void ProjectStarted(object sender, ProjectStartedEventArgs e) { if (!_results.TryGetValue(tfm, out AnalyzerResult result)) { - result = new AnalyzerResult(_analyzer); + result = new AnalyzerResult(_projectFilePath, _manager, _analyzer); _results[tfm] = result; } result.ProcessProject(e); @@ -118,9 +129,9 @@ public void Dispose() } // Need to release the loggers in case they get used again (I.e., Restore followed by Clean;Build) - foreach (Microsoft.Build.Framework.ILogger logger in _loggers) + foreach (Microsoft.Build.Framework.ILogger buildLogger in _buildLoggers) { - logger.Shutdown(); + buildLogger.Shutdown(); } } } diff --git a/src/Buildalyzer/ProjectAnalyzer.cs b/src/Buildalyzer/ProjectAnalyzer.cs index 568ae80b..cb06a289 100644 --- a/src/Buildalyzer/ProjectAnalyzer.cs +++ b/src/Buildalyzer/ProjectAnalyzer.cs @@ -18,7 +18,7 @@ namespace Buildalyzer { public class ProjectAnalyzer { - private readonly List _loggers = new List(); + private readonly List _buildLoggers = new List(); // Project-specific global properties and environment variables private readonly ConcurrentDictionary _globalProperties = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); @@ -56,7 +56,7 @@ public class ProjectAnalyzer /// public IReadOnlyDictionary EnvironmentVariables => GetEffectiveEnvironmentVariables(null); - public IEnumerable BuildLoggers => _loggers; + public IEnumerable BuildLoggers => _buildLoggers; public ILogger Logger { get; set; } @@ -65,7 +65,6 @@ public class ProjectAnalyzer /// public bool IgnoreFaultyImports { get; set; } = true; - // The project file path should already be normalized internal ProjectAnalyzer(AnalyzerManager manager, string projectFilePath, ProjectInSolution projectInSolution) { @@ -235,7 +234,7 @@ private AnalyzerResults BuildTargets(BuildEnvironment buildEnvironment, string t { using (AnonymousPipeLoggerServer pipeLogger = new AnonymousPipeLoggerServer(cancellation.Token)) { - using (EventProcessor eventProcessor = new EventProcessor(this, BuildLoggers, pipeLogger, results != null)) + using (EventProcessor eventProcessor = new EventProcessor(Manager, this, BuildLoggers, pipeLogger, results != null)) { // Run MSBuild int exitCode; @@ -271,7 +270,7 @@ private string GetCommand(BuildEnvironment buildEnvironment, string targetFramew // Get the logger arguments string loggerPath = typeof(BuildalyzerLogger).Assembly.Location; - bool logEverything = _loggers.Count > 0; + bool logEverything = _buildLoggers.Count > 0; string loggerArgument = $"/l:{nameof(BuildalyzerLogger)},{FormatArgument(loggerPath)};{pipeLoggerClientHandle};{logEverything}"; // Get the properties arguments @@ -385,7 +384,7 @@ public void AddBuildLogger(ILogger logger) throw new ArgumentNullException(nameof(logger)); } - _loggers.Add(logger); + _buildLoggers.Add(logger); } /// @@ -399,7 +398,7 @@ public void RemoveBuildLogger(ILogger logger) throw new ArgumentNullException(nameof(logger)); } - _loggers.Remove(logger); + _buildLoggers.Remove(logger); } } } \ No newline at end of file diff --git a/tests/Buildalyzer.Tests/Integration/OpenSourceProjectsFixture.cs b/tests/Buildalyzer.Tests/Integration/OpenSourceProjectsFixture.cs index 16230ae7..4a0672aa 100644 --- a/tests/Buildalyzer.Tests/Integration/OpenSourceProjectsFixture.cs +++ b/tests/Buildalyzer.Tests/Integration/OpenSourceProjectsFixture.cs @@ -31,7 +31,8 @@ public class OpenSourceProjectsFixture private static TestRepository[] Repositories = { - new TestRepository("https://github.com/autofac/Autofac.git"), + new TestRepository("https://github.com/autofac/Autofac.git", + @"\bench\Autofac.Benchmarks\Autofac.Benchmarks.csproj"), new TestRepository("https://github.com/AutoMapper/AutoMapper.git"), new TestRepository(EnvironmentPreference.Framework, "https://github.com/JamesNK/Newtonsoft.Json.git"), // Contains portable project, can't build using SDK new TestRepository("https://github.com/nodatime/nodatime.git", diff --git a/tests/Buildalyzer.Tests/Integration/SimpleProjectsFixture.cs b/tests/Buildalyzer.Tests/Integration/SimpleProjectsFixture.cs index 135607f9..52c60641 100644 --- a/tests/Buildalyzer.Tests/Integration/SimpleProjectsFixture.cs +++ b/tests/Buildalyzer.Tests/Integration/SimpleProjectsFixture.cs @@ -144,6 +144,45 @@ public void GetsReferences( } } + [Test] + public void GetsSourceFilesFromBinaryLog( + [ValueSource(nameof(Preferences))] EnvironmentPreference preference, + [ValueSource(nameof(ProjectFiles))] string projectFile) + { + // Given + StringWriter log = new StringWriter(); + ProjectAnalyzer analyzer = GetProjectAnalyzer(projectFile, log); + EnvironmentOptions options = new EnvironmentOptions + { + Preference = preference + }; + string binLogPath = Path.ChangeExtension(Path.GetTempFileName(), ".binlog"); + analyzer.AddBinaryLogger(binLogPath); + + try + { + // When + analyzer.Build(options); + IReadOnlyList sourceFiles = analyzer.Manager.Analyze(binLogPath).First().SourceFiles; + + // Then + sourceFiles.ShouldNotBeNull(log.ToString()); + new[] + { + "Class1", + "AssemblyAttributes", + "AssemblyInfo" + }.ShouldBeSubsetOf(sourceFiles.Select(x => Path.GetFileName(x).Split('.').TakeLast(2).First()), log.ToString()); + } + finally + { + if(File.Exists(binLogPath)) + { + File.Delete(binLogPath); + } + } + } + #if Is_Windows [Test] public void MultiTargetingBuildAllTargetFrameworksGetsSourceFiles()