diff --git a/src/Incrementalist.Cmd/Commands/LoadSolutionCmd.cs b/src/Incrementalist.Cmd/Commands/LoadSolutionCmd.cs index d27afd3..23bc11d 100644 --- a/src/Incrementalist.Cmd/Commands/LoadSolutionCmd.cs +++ b/src/Incrementalist.Cmd/Commands/LoadSolutionCmd.cs @@ -42,7 +42,20 @@ protected override async Task ProcessImpl(Task previousTask) $"solution filename. Instead returned {slnObject}"); var slnName = slnObject; Contract.Assert(File.Exists(slnName), $"Expected to find {slnName} on the file system, but couldn't."); + + // Log any solution loading issues + _workspace.WorkspaceFailed += (sender, args) => + { + var message = $"Issue during solution loading: {sender}: {args.Diagnostic.Message}"; + var logLevel = args.Diagnostic.Kind == WorkspaceDiagnosticKind.Failure ? LogLevel.Error : LogLevel.Warning; + + Logger.Log(logLevel, message); + }; + // Roslyn does not support FSharp projects, but .fsproj has same structure as .csproj files, + // so can treat them as a known project type to support diff tracking + _workspace.AssociateFileExtensionWithLanguage("fsproj", LanguageNames.CSharp); + return await _workspace.OpenSolutionAsync(slnName, _progress, CancellationToken); } } diff --git a/src/Incrementalist.Tests/Dependencies/FSharpProjectsTrackingSpecs.cs b/src/Incrementalist.Tests/Dependencies/FSharpProjectsTrackingSpecs.cs new file mode 100644 index 0000000..b8cd180 --- /dev/null +++ b/src/Incrementalist.Tests/Dependencies/FSharpProjectsTrackingSpecs.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Incrementalist.Cmd.Commands; +using Incrementalist.ProjectSystem; +using Incrementalist.ProjectSystem.Cmds; +using Incrementalist.Tests.Helpers; +using Microsoft.Build.Locator; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.MSBuild; +using Xunit; +using Xunit.Abstractions; + +namespace Incrementalist.Tests.Dependencies +{ + public class FSharpProjectsTrackingSpecs : IDisposable + { + private readonly ITestOutputHelper _outputHelper; + public DisposableRepository Repository { get; } + + public FSharpProjectsTrackingSpecs(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + Repository = new DisposableRepository(); + } + + public void Dispose() + { + Repository?.Dispose(); + } + + [Fact] + public async Task FSharpProjectDiff_should_be_tracked() + { + var sample = ProjectSampleGenerator.GetFSharpSolutionSample("FSharpSolution.sln"); + var solutionFullPath = sample.SolutionFile.GetFullPath(Repository.BasePath); + var fsharpProjectFullPath = sample.FSharpProjectFile.GetFullPath(Repository.BasePath); + var csharpProjectFullPath = sample.CSharpProjectFile.GetFullPath(Repository.BasePath); + + Repository + .WriteFile(sample.SolutionFile) + .WriteFile(sample.CSharpProjectFile) + .WriteFile(sample.FSharpProjectFile) + .Commit("Created new solution with fsharp and csharp projects") + .CreateBranch("foo") + .CheckoutBranch("foo") + .WriteFile(sample.CSharpProjectFile.Name, sample.CSharpProjectFile.Content + " ") + .WriteFile(sample.FSharpProjectFile.Name, sample.FSharpProjectFile.Content + " ") + .Commit("Updated both project files"); + + var logger = new TestOutputLogger(_outputHelper); + var settings = new BuildSettings("master", solutionFullPath, Repository.BasePath); + var workspace = SetupMsBuildWorkspace(); + var emitTask = new EmitDependencyGraphTask(settings, workspace, logger); + var affectedFiles = (await emitTask.Run()).ToList(); + + affectedFiles.Select(f => f.Key).Should().HaveCount(2).And.Subject.Should().BeEquivalentTo(fsharpProjectFullPath, csharpProjectFullPath); + } + + private static MSBuildWorkspace SetupMsBuildWorkspace() + { + // Locate and register the default instance of MSBuild installed on this machine. + MSBuildLocator.RegisterDefaults(); + + return MSBuildWorkspace.Create(); + } + } +} \ No newline at end of file diff --git a/src/Incrementalist.Tests/Dependencies/ProjectImportsTrackingSpecs.cs b/src/Incrementalist.Tests/Dependencies/ProjectImportsTrackingSpecs.cs index d1ddf68..12385d7 100644 --- a/src/Incrementalist.Tests/Dependencies/ProjectImportsTrackingSpecs.cs +++ b/src/Incrementalist.Tests/Dependencies/ProjectImportsTrackingSpecs.cs @@ -34,14 +34,13 @@ public void Dispose() [Fact(DisplayName = "List of project imported files should be loaded correctly")] public void ImportedFilePathIsFound() { - var sample = ProjectSampleGenerator.GetProjectWithImportSample(); - var projectName = "SampleProject.csproj"; - var projectFilePath = Path.Combine(Repository.BasePath, projectName); + var sample = ProjectSampleGenerator.GetProjectWithImportSample("SampleProject.csproj"); + var projectFilePath = sample.ProjectFile.GetFullPath(Repository.BasePath); var importedPropsFilePath = Path.Combine(Repository.BasePath, sample.ImportedPropsFile.Name); Repository - .WriteFile(projectName, sample.ProjectFileContent) - .WriteFile(sample.ImportedPropsFile.Name, sample.ImportedPropsFile.Content); + .WriteFile(sample.ProjectFile) + .WriteFile(sample.ImportedPropsFile); var projectFile = new SlnFileWithPath(projectFilePath, new SlnFile(FileType.Project, ProjectId.CreateNewId())) ; var imports = ProjectImportsFinder.FindProjectImports(new[] { projectFile }); @@ -52,14 +51,12 @@ public void ImportedFilePathIsFound() [Fact(DisplayName = "When project imported file is changed, the project should be marked as affected")] public async Task Should_mark_project_as_changed_when_only_imported_file_changed() { - var sample = ProjectSampleGenerator.GetProjectWithImportSample(); - var projectName = "SampleProject.csproj"; - var projectFilePath = Path.Combine(Repository.BasePath, projectName); - var importedPropsFilePath = Path.Combine(Repository.BasePath, sample.ImportedPropsFile.Name); + var sample = ProjectSampleGenerator.GetProjectWithImportSample("SampleProject.csproj"); + var projectFilePath = sample.ProjectFile.GetFullPath(Repository.BasePath); Repository - .WriteFile(projectName, sample.ProjectFileContent) - .WriteFile(sample.ImportedPropsFile.Name, sample.ImportedPropsFile.Content) + .WriteFile(sample.ProjectFile) + .WriteFile(sample.ImportedPropsFile) .Commit("Created sample project") .CreateBranch("foo") .CheckoutBranch("foo") diff --git a/src/Incrementalist.Tests/Helpers/DisposableRepository.cs b/src/Incrementalist.Tests/Helpers/DisposableRepository.cs index e7ac818..0fdefd1 100644 --- a/src/Incrementalist.Tests/Helpers/DisposableRepository.cs +++ b/src/Incrementalist.Tests/Helpers/DisposableRepository.cs @@ -104,6 +104,14 @@ public DisposableRepository WriteFile(string fileName, string fileText) return this; } + /// + /// Adds or updates sample fime in the repository + /// + /// File info source + /// The current . + public DisposableRepository WriteFile(ProjectSampleGenerator.SampleFile sampleFile) => + WriteFile(sampleFile.Name, sampleFile.Content); + /// /// Delete an existing file from the repository. /// diff --git a/src/Incrementalist.Tests/Helpers/ProjectSampleGenerator.cs b/src/Incrementalist.Tests/Helpers/ProjectSampleGenerator.cs index 88ecb16..30544d3 100644 --- a/src/Incrementalist.Tests/Helpers/ProjectSampleGenerator.cs +++ b/src/Incrementalist.Tests/Helpers/ProjectSampleGenerator.cs @@ -1,3 +1,4 @@ +using System; using System.IO; namespace Incrementalist.Tests.Helpers @@ -10,12 +11,29 @@ public static class ProjectSampleGenerator /// /// Gets project with import files sample /// - public static ProjectWithImportSample GetProjectWithImportSample() + public static ProjectWithImportSample GetProjectWithImportSample(string projectFileName) { - var projectContent = File.ReadAllText("../../../Samples/ProjectFileWithImportSample.xml"); - var importedPropsContent = File.ReadAllText("../../../Samples/ImportedPropsSample.xml"); + var projectContent = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../Samples/ProjectFileWithImportSample.xml")); + var importedPropsContent = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../Samples/ImportedPropsSample.xml")); - return new ProjectWithImportSample(projectContent, new SampleFile("imported.props", importedPropsContent)); + return new ProjectWithImportSample( + new SampleFile(projectFileName, projectContent), + new SampleFile("imported.props", importedPropsContent)); + } + + /// + /// Gets .net solution with different csharp and fsharp projects + /// + public static FSharpSampleSolution GetFSharpSolutionSample(string solutionName) + { + var solutionContent = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../Samples/FSharpSampleSolution/Solution.xml")); + var fsharpProjectContent = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../Samples/FSharpSampleSolution/FSharpProject.xml")); + var csharpProjectContent = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../Samples/FSharpSampleSolution/CSharpProject.xml")); + + return new FSharpSampleSolution( + new SampleFile(solutionName, solutionContent), + new SampleFile("CSharpProject.csproj", csharpProjectContent), + new SampleFile("FSharpProject.fsproj", fsharpProjectContent)); } /// @@ -23,9 +41,9 @@ public static ProjectWithImportSample GetProjectWithImportSample() /// public class ProjectWithImportSample { - public ProjectWithImportSample(string projectFileContent, SampleFile importedPropsFile) + public ProjectWithImportSample(SampleFile projectFile, SampleFile importedPropsFile) { - ProjectFileContent = projectFileContent; + ProjectFile = projectFile; ImportedPropsFile = importedPropsFile; } @@ -35,7 +53,7 @@ public ProjectWithImportSample(string projectFileContent, SampleFile importedPro /// /// Name of the file is not important here /// - public string ProjectFileContent { get; } + public SampleFile ProjectFile { get; } /// /// Imported props file info /// @@ -44,7 +62,33 @@ public ProjectWithImportSample(string projectFileContent, SampleFile importedPro /// public SampleFile ImportedPropsFile { get; } } + + /// + /// FSharp sample solution data + /// + public class FSharpSampleSolution + { + public FSharpSampleSolution(SampleFile solutionFile, SampleFile fSharpProjectFile, SampleFile cSharpProjectFile) + { + SolutionFile = solutionFile; + FSharpProjectFile = fSharpProjectFile; + CSharpProjectFile = cSharpProjectFile; + } + /// + /// Solution file info. + /// + public SampleFile SolutionFile { get; } + /// + /// FSharp project file info. Name of the project is used in solution's content + /// + public SampleFile FSharpProjectFile { get; } + /// + /// CSharp project file info. Name of the project is used in solution's content + /// + public SampleFile CSharpProjectFile { get; } + } + /// /// Generated sample file info /// @@ -64,6 +108,11 @@ public SampleFile(string name, string content) /// File content /// public string Content { get; } + + /// + /// Gets full file path + /// + public string GetFullPath(string basePath) => Path.Combine(basePath, Name); } } } \ No newline at end of file diff --git a/src/Incrementalist.Tests/Incrementalist.Tests.csproj b/src/Incrementalist.Tests/Incrementalist.Tests.csproj index 5edd1a6..fef88c0 100644 --- a/src/Incrementalist.Tests/Incrementalist.Tests.csproj +++ b/src/Incrementalist.Tests/Incrementalist.Tests.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Incrementalist.Tests/Samples/FSharpSampleSolution/CSharpProject.xml b/src/Incrementalist.Tests/Samples/FSharpSampleSolution/CSharpProject.xml new file mode 100644 index 0000000..17ca88b --- /dev/null +++ b/src/Incrementalist.Tests/Samples/FSharpSampleSolution/CSharpProject.xml @@ -0,0 +1,7 @@ + + + + netcoreapp2.2 + + + diff --git a/src/Incrementalist.Tests/Samples/FSharpSampleSolution/FSharpProject.xml b/src/Incrementalist.Tests/Samples/FSharpSampleSolution/FSharpProject.xml new file mode 100644 index 0000000..ec0b0b9 --- /dev/null +++ b/src/Incrementalist.Tests/Samples/FSharpSampleSolution/FSharpProject.xml @@ -0,0 +1,7 @@ + + + + netcoreapp2.2 + + + diff --git a/src/Incrementalist.Tests/Samples/FSharpSampleSolution/Solution.xml b/src/Incrementalist.Tests/Samples/FSharpSampleSolution/Solution.xml new file mode 100644 index 0000000..3508e2a --- /dev/null +++ b/src/Incrementalist.Tests/Samples/FSharpSampleSolution/Solution.xml @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharpProject", "FSharpProject.fsproj", "{50FA647B-BAE4-4DF5-99CA-934B8227E236}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpProject", "CSharpProject.csproj", "{5FEC8D4E-75A8-4730-A57D-8258B2CD971C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {50FA647B-BAE4-4DF5-99CA-934B8227E236}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50FA647B-BAE4-4DF5-99CA-934B8227E236}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50FA647B-BAE4-4DF5-99CA-934B8227E236}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50FA647B-BAE4-4DF5-99CA-934B8227E236}.Release|Any CPU.Build.0 = Release|Any CPU + {5FEC8D4E-75A8-4730-A57D-8258B2CD971C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5FEC8D4E-75A8-4730-A57D-8258B2CD971C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5FEC8D4E-75A8-4730-A57D-8258B2CD971C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5FEC8D4E-75A8-4730-A57D-8258B2CD971C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/src/Incrementalist/ProjectSystem/Cmds/FilterAffectedProjectFilesCmd.cs b/src/Incrementalist/ProjectSystem/Cmds/FilterAffectedProjectFilesCmd.cs index 9a1b866..230a76b 100644 --- a/src/Incrementalist/ProjectSystem/Cmds/FilterAffectedProjectFilesCmd.cs +++ b/src/Incrementalist/ProjectSystem/Cmds/FilterAffectedProjectFilesCmd.cs @@ -58,7 +58,7 @@ protected override async Task> ProcessImpl( var affectedFiles = DiffHelper.ChangedFiles(repo, _targetGitBranch).ToList(); var projectFiles = fileDict.Where(x => x.Value.FileType == FileType.Project).ToList(); - var projectFolders = projectFiles.ToDictionary(x => Path.GetDirectoryName(x.Key), v => Tuple.Create(v.Key, v.Value)); + var projectFolders = projectFiles.ToLookup(x => Path.GetDirectoryName(x.Key), v => Tuple.Create(v.Key, v.Value)); var projectImports = ProjectImportsFinder.FindProjectImports(projectFiles.Select(pair => new SlnFileWithPath(pair.Key, pair.Value))); // filter out any files that aren't affected by the diff @@ -74,13 +74,17 @@ protected override async Task> ProcessImpl( // Check to see if these affected files are in the same folder as any of the projects var directoryName = Path.GetDirectoryName(file); - if (TryFindSubFolder(projectFolders.Keys, directoryName, out var projectFolder)) + if (TryFindSubFolder(projectFolders.Select(c => c.Key), directoryName, out var projectFolder)) { - var project = projectFolders[projectFolder].Item2; - var projectPath = projectFolders[projectFolder].Item1; - Logger.LogInformation("Adding project {0} to the set of affected files because non-code file {1}, " + - "found inside same directory [{2}], was modified.", projectPath, file, directoryName); - newDict[projectPath] = project; + var affectedProjects = projectFolders[projectFolder]; + foreach (var affectedProject in affectedProjects) + { + var project = affectedProject.Item2; + var projectPath = affectedProject.Item1; + Logger.LogInformation("Adding project {0} to the set of affected files because non-code file {1}, " + + "found inside same directory [{2}], was modified.", projectPath, file, directoryName); + newDict[projectPath] = project; + } } }