Skip to content

Commit 0cd8f96

Browse files
IgorFedchenkoAaronontheweb
authored andcommitted
Fsharp projects change tracking (#72)
* Added imported project files modifications check * Added some tests + refactoring * Added test for fsharp dependency tracking before implementation * Fixed issue when multiple projects share same directory * Added F# project loading hack * Handle working directory change during tests execution * close #72
1 parent 9b1d69d commit 0cd8f96

File tree

10 files changed

+204
-25
lines changed

10 files changed

+204
-25
lines changed

src/Incrementalist.Cmd/Commands/LoadSolutionCmd.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,20 @@ protected override async Task<Solution> ProcessImpl(Task<string> previousTask)
4242
$"solution filename. Instead returned {slnObject}");
4343
var slnName = slnObject;
4444
Contract.Assert(File.Exists(slnName), $"Expected to find {slnName} on the file system, but couldn't.");
45+
46+
// Log any solution loading issues
47+
_workspace.WorkspaceFailed += (sender, args) =>
48+
{
49+
var message = $"Issue during solution loading: {sender}: {args.Diagnostic.Message}";
50+
var logLevel = args.Diagnostic.Kind == WorkspaceDiagnosticKind.Failure ? LogLevel.Error : LogLevel.Warning;
51+
52+
Logger.Log(logLevel, message);
53+
};
4554

55+
// Roslyn does not support FSharp projects, but .fsproj has same structure as .csproj files,
56+
// so can treat them as a known project type to support diff tracking
57+
_workspace.AssociateFileExtensionWithLanguage("fsproj", LanguageNames.CSharp);
58+
4659
return await _workspace.OpenSolutionAsync(slnName, _progress, CancellationToken);
4760
}
4861
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using FluentAssertions;
7+
using Incrementalist.Cmd.Commands;
8+
using Incrementalist.ProjectSystem;
9+
using Incrementalist.ProjectSystem.Cmds;
10+
using Incrementalist.Tests.Helpers;
11+
using Microsoft.Build.Locator;
12+
using Microsoft.CodeAnalysis;
13+
using Microsoft.CodeAnalysis.MSBuild;
14+
using Xunit;
15+
using Xunit.Abstractions;
16+
17+
namespace Incrementalist.Tests.Dependencies
18+
{
19+
public class FSharpProjectsTrackingSpecs : IDisposable
20+
{
21+
private readonly ITestOutputHelper _outputHelper;
22+
public DisposableRepository Repository { get; }
23+
24+
public FSharpProjectsTrackingSpecs(ITestOutputHelper outputHelper)
25+
{
26+
_outputHelper = outputHelper;
27+
Repository = new DisposableRepository();
28+
}
29+
30+
public void Dispose()
31+
{
32+
Repository?.Dispose();
33+
}
34+
35+
[Fact]
36+
public async Task FSharpProjectDiff_should_be_tracked()
37+
{
38+
var sample = ProjectSampleGenerator.GetFSharpSolutionSample("FSharpSolution.sln");
39+
var solutionFullPath = sample.SolutionFile.GetFullPath(Repository.BasePath);
40+
var fsharpProjectFullPath = sample.FSharpProjectFile.GetFullPath(Repository.BasePath);
41+
var csharpProjectFullPath = sample.CSharpProjectFile.GetFullPath(Repository.BasePath);
42+
43+
Repository
44+
.WriteFile(sample.SolutionFile)
45+
.WriteFile(sample.CSharpProjectFile)
46+
.WriteFile(sample.FSharpProjectFile)
47+
.Commit("Created new solution with fsharp and csharp projects")
48+
.CreateBranch("foo")
49+
.CheckoutBranch("foo")
50+
.WriteFile(sample.CSharpProjectFile.Name, sample.CSharpProjectFile.Content + " ")
51+
.WriteFile(sample.FSharpProjectFile.Name, sample.FSharpProjectFile.Content + " ")
52+
.Commit("Updated both project files");
53+
54+
var logger = new TestOutputLogger(_outputHelper);
55+
var settings = new BuildSettings("master", solutionFullPath, Repository.BasePath);
56+
var workspace = SetupMsBuildWorkspace();
57+
var emitTask = new EmitDependencyGraphTask(settings, workspace, logger);
58+
var affectedFiles = (await emitTask.Run()).ToList();
59+
60+
affectedFiles.Select(f => f.Key).Should().HaveCount(2).And.Subject.Should().BeEquivalentTo(fsharpProjectFullPath, csharpProjectFullPath);
61+
}
62+
63+
private static MSBuildWorkspace SetupMsBuildWorkspace()
64+
{
65+
// Locate and register the default instance of MSBuild installed on this machine.
66+
MSBuildLocator.RegisterDefaults();
67+
68+
return MSBuildWorkspace.Create();
69+
}
70+
}
71+
}

src/Incrementalist.Tests/Dependencies/ProjectImportsTrackingSpecs.cs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,13 @@ public void Dispose()
3434
[Fact(DisplayName = "List of project imported files should be loaded correctly")]
3535
public void ImportedFilePathIsFound()
3636
{
37-
var sample = ProjectSampleGenerator.GetProjectWithImportSample();
38-
var projectName = "SampleProject.csproj";
39-
var projectFilePath = Path.Combine(Repository.BasePath, projectName);
37+
var sample = ProjectSampleGenerator.GetProjectWithImportSample("SampleProject.csproj");
38+
var projectFilePath = sample.ProjectFile.GetFullPath(Repository.BasePath);
4039
var importedPropsFilePath = Path.Combine(Repository.BasePath, sample.ImportedPropsFile.Name);
4140

4241
Repository
43-
.WriteFile(projectName, sample.ProjectFileContent)
44-
.WriteFile(sample.ImportedPropsFile.Name, sample.ImportedPropsFile.Content);
42+
.WriteFile(sample.ProjectFile)
43+
.WriteFile(sample.ImportedPropsFile);
4544

4645
var projectFile = new SlnFileWithPath(projectFilePath, new SlnFile(FileType.Project, ProjectId.CreateNewId())) ;
4746
var imports = ProjectImportsFinder.FindProjectImports(new[] { projectFile });
@@ -52,14 +51,12 @@ public void ImportedFilePathIsFound()
5251
[Fact(DisplayName = "When project imported file is changed, the project should be marked as affected")]
5352
public async Task Should_mark_project_as_changed_when_only_imported_file_changed()
5453
{
55-
var sample = ProjectSampleGenerator.GetProjectWithImportSample();
56-
var projectName = "SampleProject.csproj";
57-
var projectFilePath = Path.Combine(Repository.BasePath, projectName);
58-
var importedPropsFilePath = Path.Combine(Repository.BasePath, sample.ImportedPropsFile.Name);
54+
var sample = ProjectSampleGenerator.GetProjectWithImportSample("SampleProject.csproj");
55+
var projectFilePath = sample.ProjectFile.GetFullPath(Repository.BasePath);
5956

6057
Repository
61-
.WriteFile(projectName, sample.ProjectFileContent)
62-
.WriteFile(sample.ImportedPropsFile.Name, sample.ImportedPropsFile.Content)
58+
.WriteFile(sample.ProjectFile)
59+
.WriteFile(sample.ImportedPropsFile)
6360
.Commit("Created sample project")
6461
.CreateBranch("foo")
6562
.CheckoutBranch("foo")

src/Incrementalist.Tests/Helpers/DisposableRepository.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ public DisposableRepository WriteFile(string fileName, string fileText)
104104
return this;
105105
}
106106

107+
/// <summary>
108+
/// Adds or updates sample fime in the repository
109+
/// </summary>
110+
/// <param name="sampleFile">File info source</param>
111+
/// <returns>The current <see cref="DisposableRepository" />.</returns>
112+
public DisposableRepository WriteFile(ProjectSampleGenerator.SampleFile sampleFile) =>
113+
WriteFile(sampleFile.Name, sampleFile.Content);
114+
107115
/// <summary>
108116
/// Delete an existing file from the repository.
109117
/// </summary>

src/Incrementalist.Tests/Helpers/ProjectSampleGenerator.cs

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.IO;
23

34
namespace Incrementalist.Tests.Helpers
@@ -10,22 +11,39 @@ public static class ProjectSampleGenerator
1011
/// <summary>
1112
/// Gets project with import files sample
1213
/// </summary>
13-
public static ProjectWithImportSample GetProjectWithImportSample()
14+
public static ProjectWithImportSample GetProjectWithImportSample(string projectFileName)
1415
{
15-
var projectContent = File.ReadAllText("../../../Samples/ProjectFileWithImportSample.xml");
16-
var importedPropsContent = File.ReadAllText("../../../Samples/ImportedPropsSample.xml");
16+
var projectContent = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../Samples/ProjectFileWithImportSample.xml"));
17+
var importedPropsContent = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../Samples/ImportedPropsSample.xml"));
1718

18-
return new ProjectWithImportSample(projectContent, new SampleFile("imported.props", importedPropsContent));
19+
return new ProjectWithImportSample(
20+
new SampleFile(projectFileName, projectContent),
21+
new SampleFile("imported.props", importedPropsContent));
22+
}
23+
24+
/// <summary>
25+
/// Gets .net solution with different csharp and fsharp projects
26+
/// </summary>
27+
public static FSharpSampleSolution GetFSharpSolutionSample(string solutionName)
28+
{
29+
var solutionContent = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../Samples/FSharpSampleSolution/Solution.xml"));
30+
var fsharpProjectContent = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../Samples/FSharpSampleSolution/FSharpProject.xml"));
31+
var csharpProjectContent = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../Samples/FSharpSampleSolution/CSharpProject.xml"));
32+
33+
return new FSharpSampleSolution(
34+
new SampleFile(solutionName, solutionContent),
35+
new SampleFile("CSharpProject.csproj", csharpProjectContent),
36+
new SampleFile("FSharpProject.fsproj", fsharpProjectContent));
1937
}
2038

2139
/// <summary>
2240
/// Sample files required for tests with project imports
2341
/// </summary>
2442
public class ProjectWithImportSample
2543
{
26-
public ProjectWithImportSample(string projectFileContent, SampleFile importedPropsFile)
44+
public ProjectWithImportSample(SampleFile projectFile, SampleFile importedPropsFile)
2745
{
28-
ProjectFileContent = projectFileContent;
46+
ProjectFile = projectFile;
2947
ImportedPropsFile = importedPropsFile;
3048
}
3149

@@ -35,7 +53,7 @@ public ProjectWithImportSample(string projectFileContent, SampleFile importedPro
3553
/// <remarks>
3654
/// Name of the file is not important here
3755
/// </remarks>
38-
public string ProjectFileContent { get; }
56+
public SampleFile ProjectFile { get; }
3957
/// <summary>
4058
/// Imported props file info
4159
/// </summary>
@@ -44,7 +62,33 @@ public ProjectWithImportSample(string projectFileContent, SampleFile importedPro
4462
/// </remarks>
4563
public SampleFile ImportedPropsFile { get; }
4664
}
65+
66+
/// <summary>
67+
/// FSharp sample solution data
68+
/// </summary>
69+
public class FSharpSampleSolution
70+
{
71+
public FSharpSampleSolution(SampleFile solutionFile, SampleFile fSharpProjectFile, SampleFile cSharpProjectFile)
72+
{
73+
SolutionFile = solutionFile;
74+
FSharpProjectFile = fSharpProjectFile;
75+
CSharpProjectFile = cSharpProjectFile;
76+
}
4777

78+
/// <summary>
79+
/// Solution file info.
80+
/// </summary>
81+
public SampleFile SolutionFile { get; }
82+
/// <summary>
83+
/// FSharp project file info. Name of the project is used in solution's content
84+
/// </summary>
85+
public SampleFile FSharpProjectFile { get; }
86+
/// <summary>
87+
/// CSharp project file info. Name of the project is used in solution's content
88+
/// </summary>
89+
public SampleFile CSharpProjectFile { get; }
90+
}
91+
4892
/// <summary>
4993
/// Generated sample file info
5094
/// </summary>
@@ -64,6 +108,11 @@ public SampleFile(string name, string content)
64108
/// File content
65109
/// </summary>
66110
public string Content { get; }
111+
112+
/// <summary>
113+
/// Gets full file path
114+
/// </summary>
115+
public string GetFullPath(string basePath) => Path.Combine(basePath, Name);
67116
}
68117
}
69118
}

src/Incrementalist.Tests/Incrementalist.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
</ItemGroup>
1414

1515
<ItemGroup>
16+
<ProjectReference Include="..\Incrementalist.Cmd\Incrementalist.Cmd.csproj" />
1617
<ProjectReference Include="..\Incrementalist\Incrementalist.csproj" />
1718
</ItemGroup>
1819

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netcoreapp2.2</TargetFramework>
5+
</PropertyGroup>
6+
7+
</Project>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netcoreapp2.2</TargetFramework>
5+
</PropertyGroup>
6+
7+
</Project>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharpProject", "FSharpProject.fsproj", "{50FA647B-BAE4-4DF5-99CA-934B8227E236}"
4+
EndProject
5+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpProject", "CSharpProject.csproj", "{5FEC8D4E-75A8-4730-A57D-8258B2CD971C}"
6+
EndProject
7+
Global
8+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
9+
Debug|Any CPU = Debug|Any CPU
10+
Release|Any CPU = Release|Any CPU
11+
EndGlobalSection
12+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
13+
{50FA647B-BAE4-4DF5-99CA-934B8227E236}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
14+
{50FA647B-BAE4-4DF5-99CA-934B8227E236}.Debug|Any CPU.Build.0 = Debug|Any CPU
15+
{50FA647B-BAE4-4DF5-99CA-934B8227E236}.Release|Any CPU.ActiveCfg = Release|Any CPU
16+
{50FA647B-BAE4-4DF5-99CA-934B8227E236}.Release|Any CPU.Build.0 = Release|Any CPU
17+
{5FEC8D4E-75A8-4730-A57D-8258B2CD971C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
18+
{5FEC8D4E-75A8-4730-A57D-8258B2CD971C}.Debug|Any CPU.Build.0 = Debug|Any CPU
19+
{5FEC8D4E-75A8-4730-A57D-8258B2CD971C}.Release|Any CPU.ActiveCfg = Release|Any CPU
20+
{5FEC8D4E-75A8-4730-A57D-8258B2CD971C}.Release|Any CPU.Build.0 = Release|Any CPU
21+
EndGlobalSection
22+
EndGlobal

src/Incrementalist/ProjectSystem/Cmds/FilterAffectedProjectFilesCmd.cs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ protected override async Task<Dictionary<string, SlnFile>> ProcessImpl(
5858
var affectedFiles = DiffHelper.ChangedFiles(repo, _targetGitBranch).ToList();
5959

6060
var projectFiles = fileDict.Where(x => x.Value.FileType == FileType.Project).ToList();
61-
var projectFolders = projectFiles.ToDictionary(x => Path.GetDirectoryName(x.Key), v => Tuple.Create(v.Key, v.Value));
61+
var projectFolders = projectFiles.ToLookup(x => Path.GetDirectoryName(x.Key), v => Tuple.Create(v.Key, v.Value));
6262
var projectImports = ProjectImportsFinder.FindProjectImports(projectFiles.Select(pair => new SlnFileWithPath(pair.Key, pair.Value)));
6363

6464
// filter out any files that aren't affected by the diff
@@ -74,13 +74,17 @@ protected override async Task<Dictionary<string, SlnFile>> ProcessImpl(
7474
// Check to see if these affected files are in the same folder as any of the projects
7575
var directoryName = Path.GetDirectoryName(file);
7676

77-
if (TryFindSubFolder(projectFolders.Keys, directoryName, out var projectFolder))
77+
if (TryFindSubFolder(projectFolders.Select(c => c.Key), directoryName, out var projectFolder))
7878
{
79-
var project = projectFolders[projectFolder].Item2;
80-
var projectPath = projectFolders[projectFolder].Item1;
81-
Logger.LogInformation("Adding project {0} to the set of affected files because non-code file {1}, " +
82-
"found inside same directory [{2}], was modified.", projectPath, file, directoryName);
83-
newDict[projectPath] = project;
79+
var affectedProjects = projectFolders[projectFolder];
80+
foreach (var affectedProject in affectedProjects)
81+
{
82+
var project = affectedProject.Item2;
83+
var projectPath = affectedProject.Item1;
84+
Logger.LogInformation("Adding project {0} to the set of affected files because non-code file {1}, " +
85+
"found inside same directory [{2}], was modified.", projectPath, file, directoryName);
86+
newDict[projectPath] = project;
87+
}
8488
}
8589
}
8690

0 commit comments

Comments
 (0)