Skip to content

Commit

Permalink
Fsharp projects change tracking (#72)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
IgorFedchenko authored and Aaronontheweb committed Oct 28, 2019
1 parent 9b1d69d commit 0cd8f96
Show file tree
Hide file tree
Showing 10 changed files with 204 additions and 25 deletions.
13 changes: 13 additions & 0 deletions src/Incrementalist.Cmd/Commands/LoadSolutionCmd.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,20 @@ protected override async Task<Solution> ProcessImpl(Task<string> 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);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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")
Expand Down
8 changes: 8 additions & 0 deletions src/Incrementalist.Tests/Helpers/DisposableRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ public DisposableRepository WriteFile(string fileName, string fileText)
return this;
}

/// <summary>
/// Adds or updates sample fime in the repository
/// </summary>
/// <param name="sampleFile">File info source</param>
/// <returns>The current <see cref="DisposableRepository" />.</returns>
public DisposableRepository WriteFile(ProjectSampleGenerator.SampleFile sampleFile) =>
WriteFile(sampleFile.Name, sampleFile.Content);

/// <summary>
/// Delete an existing file from the repository.
/// </summary>
Expand Down
63 changes: 56 additions & 7 deletions src/Incrementalist.Tests/Helpers/ProjectSampleGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.IO;

namespace Incrementalist.Tests.Helpers
Expand All @@ -10,22 +11,39 @@ public static class ProjectSampleGenerator
/// <summary>
/// Gets project with import files sample
/// </summary>
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));
}

/// <summary>
/// Gets .net solution with different csharp and fsharp projects
/// </summary>
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));
}

/// <summary>
/// Sample files required for tests with project imports
/// </summary>
public class ProjectWithImportSample
{
public ProjectWithImportSample(string projectFileContent, SampleFile importedPropsFile)
public ProjectWithImportSample(SampleFile projectFile, SampleFile importedPropsFile)
{
ProjectFileContent = projectFileContent;
ProjectFile = projectFile;
ImportedPropsFile = importedPropsFile;
}

Expand All @@ -35,7 +53,7 @@ public ProjectWithImportSample(string projectFileContent, SampleFile importedPro
/// <remarks>
/// Name of the file is not important here
/// </remarks>
public string ProjectFileContent { get; }
public SampleFile ProjectFile { get; }
/// <summary>
/// Imported props file info
/// </summary>
Expand All @@ -44,7 +62,33 @@ public ProjectWithImportSample(string projectFileContent, SampleFile importedPro
/// </remarks>
public SampleFile ImportedPropsFile { get; }
}

/// <summary>
/// FSharp sample solution data
/// </summary>
public class FSharpSampleSolution
{
public FSharpSampleSolution(SampleFile solutionFile, SampleFile fSharpProjectFile, SampleFile cSharpProjectFile)
{
SolutionFile = solutionFile;
FSharpProjectFile = fSharpProjectFile;
CSharpProjectFile = cSharpProjectFile;
}

/// <summary>
/// Solution file info.
/// </summary>
public SampleFile SolutionFile { get; }
/// <summary>
/// FSharp project file info. Name of the project is used in solution's content
/// </summary>
public SampleFile FSharpProjectFile { get; }
/// <summary>
/// CSharp project file info. Name of the project is used in solution's content
/// </summary>
public SampleFile CSharpProjectFile { get; }
}

/// <summary>
/// Generated sample file info
/// </summary>
Expand All @@ -64,6 +108,11 @@ public SampleFile(string name, string content)
/// File content
/// </summary>
public string Content { get; }

/// <summary>
/// Gets full file path
/// </summary>
public string GetFullPath(string basePath) => Path.Combine(basePath, Name);
}
}
}
1 change: 1 addition & 0 deletions src/Incrementalist.Tests/Incrementalist.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Incrementalist.Cmd\Incrementalist.Cmd.csproj" />
<ProjectReference Include="..\Incrementalist\Incrementalist.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
</PropertyGroup>

</Project>
22 changes: 22 additions & 0 deletions src/Incrementalist.Tests/Samples/FSharpSampleSolution/Solution.xml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ protected override async Task<Dictionary<string, SlnFile>> 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
Expand All @@ -74,13 +74,17 @@ protected override async Task<Dictionary<string, SlnFile>> 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;
}
}
}

Expand Down

0 comments on commit 0cd8f96

Please sign in to comment.