diff --git a/src/ProjectDiff.Core/Entrypoints/DirectoryScanEntrypointProvider.cs b/src/ProjectDiff.Core/Entrypoints/DirectoryScanEntrypointProvider.cs index b9d0fba..510f09e 100644 --- a/src/ProjectDiff.Core/Entrypoints/DirectoryScanEntrypointProvider.cs +++ b/src/ProjectDiff.Core/Entrypoints/DirectoryScanEntrypointProvider.cs @@ -1,15 +1,18 @@ using Microsoft.Build.FileSystem; using Microsoft.Build.Graph; +using Microsoft.Extensions.Logging; namespace ProjectDiff.Core.Entrypoints; public sealed class DirectoryScanEntrypointProvider : IEntrypointProvider { private readonly string _directory; + private readonly ILogger _logger; - public DirectoryScanEntrypointProvider(string directory) + public DirectoryScanEntrypointProvider(string directory, ILogger logger) { _directory = directory; + _logger = logger; } public Task> GetEntrypoints( @@ -17,6 +20,7 @@ public Task> GetEntrypoints( CancellationToken cancellationToken ) { + _logger.LogDebug("Scanning directory '{Directory}' for project files", _directory); var entrypoints = fs.EnumerateFiles(_directory, "*.csproj", SearchOption.AllDirectories) .Select(it => new ProjectGraphEntryPoint(it)); diff --git a/src/ProjectDiff.Core/Entrypoints/SolutionEntrypointProvider.cs b/src/ProjectDiff.Core/Entrypoints/SolutionEntrypointProvider.cs index b6e1576..f49e45b 100644 --- a/src/ProjectDiff.Core/Entrypoints/SolutionEntrypointProvider.cs +++ b/src/ProjectDiff.Core/Entrypoints/SolutionEntrypointProvider.cs @@ -1,5 +1,6 @@ using Microsoft.Build.FileSystem; using Microsoft.Build.Graph; +using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.SolutionPersistence.Serializer; namespace ProjectDiff.Core.Entrypoints; @@ -7,10 +8,12 @@ namespace ProjectDiff.Core.Entrypoints; public sealed class SolutionEntrypointProvider : IEntrypointProvider { private readonly FileInfo _solution; + private readonly ILogger _logger; - public SolutionEntrypointProvider(FileInfo solution) + public SolutionEntrypointProvider(FileInfo solution, ILogger logger) { _solution = solution; + _logger = logger; } public async Task> GetEntrypoints( @@ -30,7 +33,7 @@ CancellationToken cancellationToken } - private static async Task> GetProjectEntrypoints( + private async Task> GetProjectEntrypoints( FileInfo solutionFile, Stream stream, CancellationToken cancellationToken @@ -40,8 +43,14 @@ CancellationToken cancellationToken { case ".sln": { + _logger.LogDebug("Reading {SolutionFile} as a .sln file", solutionFile.FullName); var solutionModel = await SolutionSerializers.SlnFileV12.OpenAsync(stream, cancellationToken); + _logger.LogDebug( + "Found {ProjectCount} projects in solution {SolutionFile}", + solutionModel.SolutionProjects.Count, + solutionFile.FullName + ); return solutionModel.SolutionProjects .Select(it => new ProjectGraphEntryPoint(Path.GetFullPath(it.FilePath, solutionFile.DirectoryName!)) @@ -49,8 +58,14 @@ CancellationToken cancellationToken } case ".slnx": { + _logger.LogDebug("Reading {SolutionFile} as a .slnx file", solutionFile.FullName); var solutionModel = await SolutionSerializers.SlnXml.OpenAsync(stream, cancellationToken); + _logger.LogDebug( + "Found {ProjectCount} projects in solution {SolutionFile}", + solutionModel.SolutionProjects.Count, + solutionFile.FullName + ); return solutionModel.SolutionProjects .Select(it => new ProjectGraphEntryPoint(Path.GetFullPath(it.FilePath, solutionFile.DirectoryName!)) diff --git a/src/ProjectDiff.Core/GitTreeFileSystem.cs b/src/ProjectDiff.Core/GitTreeFileSystem.cs index 631c372..ad04aaf 100644 --- a/src/ProjectDiff.Core/GitTreeFileSystem.cs +++ b/src/ProjectDiff.Core/GitTreeFileSystem.cs @@ -5,6 +5,7 @@ using Microsoft.Build.Definition; using Microsoft.Build.Evaluation; using Microsoft.Build.FileSystem; +using Microsoft.Extensions.Logging; namespace ProjectDiff.Core; @@ -14,21 +15,24 @@ public sealed class GitTreeFileSystem : MSBuildFileSystemBase private readonly Tree _tree; private readonly ProjectCollection _projectCollection; private readonly Dictionary _globalProperties; + private readonly ILogger _logger; public GitTreeFileSystem( Repository repository, Tree tree, ProjectCollection projectCollection, - Dictionary globalProperties + Dictionary globalProperties, + ILogger logger ) { _repository = repository; _tree = tree; _projectCollection = projectCollection; _globalProperties = globalProperties; + _logger = logger; } - public bool LazyLoadProjects { get; set; } = true; + public bool EagerLoadProjects { get; set; } public override TextReader ReadFile(string path) { @@ -37,7 +41,7 @@ public override TextReader ReadFile(string path) return base.ReadFile(path); } - using var stream = GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); + var stream = GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); return new StreamReader(stream); } @@ -50,12 +54,12 @@ public override Stream GetFileStream(string path, FileMode mode, FileAccess acce if (mode != FileMode.Open) { - throw new ArgumentException("Only reading files is supported", nameof(mode)); + throw new NotSupportedException("Mode must be FileMode.Open"); } if (access != FileAccess.Read) { - throw new ArgumentException("Only reading files is supported", nameof(access)); + throw new NotSupportedException("Access mode must be FileAccess.Read"); } var relativePath = RelativePath(path); @@ -152,7 +156,8 @@ public override bool DirectoryExists(string path) if (!ShouldUseTree(path)) return base.DirectoryExists(path); - var entry = _tree[RelativePath(path)]; + var relativePath = RelativePath(path); + var entry = _tree[relativePath]; return entry is { TargetType: TreeEntryTargetType.Tree }; } @@ -175,7 +180,7 @@ public override bool FileExists(string path) return false; } - if (LazyLoadProjects && IsProject(entry)) + if (EagerLoadProjects && IsProject(entry)) { // HACK: Since Imports doesn't use the file system we have to manually load the projects // whenever msbuild tries to load them. @@ -183,6 +188,7 @@ public override bool FileExists(string path) { if (_projectCollection.GetLoadedProjects(path).Count == 0) { + _logger.LogDebug("Eagerly loading project from path '{Path}'", path); LoadProject(path, _globalProperties, _projectCollection); } } @@ -230,6 +236,7 @@ ProjectCollection projects throw new NotImplementedException(); } + _logger.LogInformation("Loading project from path '{Path}'", path); var relativePath = RelativePath(path); var entry = _tree[relativePath]; @@ -239,13 +246,13 @@ ProjectCollection projects throw new InvalidOperationException("Tried loading a project that is not a blob"); } - var blob = (Blob)entry.Target; + var blob = entry.Target.Peel(); using var xml = new XmlTextReader(new StringReader(blob.GetContentText())); var projectRootElement = ProjectRootElement.Create(xml, projects); projectRootElement.FullPath = path; - return Project.FromProjectRootElement( + var project = Project.FromProjectRootElement( projectRootElement, new ProjectOptions { @@ -254,6 +261,9 @@ ProjectCollection projects LoadSettings = ProjectLoadSettings.Default | ProjectLoadSettings.RecordDuplicateButNotCircularImports } ); + + _logger.LogDebug("Loaded project '{ProjectName}' from git tree '{Tree}'", project.FullPath, _tree.Sha); + return project; } diff --git a/src/ProjectDiff.Core/ProjectDiff.Core.csproj b/src/ProjectDiff.Core/ProjectDiff.Core.csproj index f720dba..f926fd3 100644 --- a/src/ProjectDiff.Core/ProjectDiff.Core.csproj +++ b/src/ProjectDiff.Core/ProjectDiff.Core.csproj @@ -14,6 +14,7 @@ all + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/ProjectDiff.Core/ProjectDiffExecutor.cs b/src/ProjectDiff.Core/ProjectDiffExecutor.cs index 00cd924..5b48c94 100644 --- a/src/ProjectDiff.Core/ProjectDiffExecutor.cs +++ b/src/ProjectDiff.Core/ProjectDiffExecutor.cs @@ -1,36 +1,24 @@ using System.Collections.Frozen; using LibGit2Sharp; using Microsoft.Build.Graph; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using ProjectDiff.Core.Entrypoints; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace ProjectDiff.Core; public class ProjectDiffExecutor { private readonly ProjectDiffExecutorOptions _options; + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; - public ProjectDiffExecutor(ProjectDiffExecutorOptions options) + public ProjectDiffExecutor(ProjectDiffExecutorOptions options, ILoggerFactory? loggerFactory = null) { _options = options; - } - - public async Task GetProjectDiff( - FileInfo solutionFile, - string baseCommitRef = "HEAD", - string? headCommitRef = null, - CancellationToken cancellationToken = default - ) - { - return await GetProjectDiff( - solutionFile.DirectoryName ?? throw new ArgumentException( - $"{nameof(solutionFile)}.DirectoryName is null", - nameof(solutionFile) - ), - new SolutionEntrypointProvider(solutionFile), - baseCommitRef, - headCommitRef, - cancellationToken - ); + _loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; + _logger = _loggerFactory.CreateLogger(); } public async Task GetProjectDiff( @@ -41,19 +29,26 @@ public async Task GetProjectDiff( CancellationToken cancellationToken = default ) { + _logger.LogDebug("Discovering repository from path '{Path}'", path); var repoPath = Repository.Discover(path); if (repoPath is null) { + _logger.LogError("Could not find a Git repository for path '{Path}'", path); return new ProjectDiffResult { Status = ProjectDiffExecutionStatus.RepositoryNotFound }; } + _logger.LogDebug("Found repository at '{RepoPath}'", repoPath); + using var repo = new Repository(repoPath); + + _logger.LogDebug("Looking up base commit '{BaseCommitRef}'", baseCommitRef); var baseCommit = repo.Lookup(baseCommitRef); if (baseCommit is null) { + _logger.LogError("Base commit '{BaseCommitRef}' not found in repository", baseCommitRef); return new ProjectDiffResult { Status = ProjectDiffExecutionStatus.BaseCommitNotFound, @@ -63,10 +58,12 @@ public async Task GetProjectDiff( Commit? headCommit; if (headCommitRef is not null) { + _logger.LogDebug("Looking up head commit '{HeadCommitRef}'", headCommitRef); headCommit = repo.Lookup(headCommitRef); if (headCommit is null) { + _logger.LogError("Head commit '{HeadCommitRef}' not found in repository", headCommitRef); return new ProjectDiffResult { Status = ProjectDiffExecutionStatus.HeadCommitNotFound @@ -75,29 +72,56 @@ public async Task GetProjectDiff( } else { + _logger.LogDebug("No head commit specified, using working directory state"); headCommit = null; } if (_options.FindMergeBase) { + _logger.LogDebug( + "Finding merge base between base commit '{BaseCommitRef}' and head commit '{HeadCommitRef}'", + baseCommitRef, + headCommitRef + ); var mergeBaseCommit = repo.ObjectDatabase.FindMergeBase(baseCommit, headCommit ?? repo.Head.Tip); if (mergeBaseCommit is null) { + _logger.LogError( + "Could not find merge base between base commit '{BaseCommitRef}' and head commit '{HeadCommitRef}'", + baseCommitRef, + headCommitRef + ); return new ProjectDiffResult { Status = ProjectDiffExecutionStatus.MergeBaseNotFound }; } + _logger.LogDebug( + "Found merge base commit '{MergeBaseCommit}'", + mergeBaseCommit.Sha + ); + baseCommit = mergeBaseCommit; } + _logger.LogInformation( + "Finding changed files between commits '{BaseCommitRef}' and '{HeadCommitRef}'", + baseCommitRef, + headCommitRef ?? "working directory" + ); var changedFiles = GetGitModifiedFiles(repo, baseCommit, headCommit) .Where(ShouldIncludeFile) .ToFrozenSet(); + if (changedFiles.Count == 0) { + _logger.LogInformation( + "No changed files found between commits '{BaseCommitRef}' and '{HeadCommitRef}'", + baseCommitRef, + headCommitRef ?? "working directory" + ); return new ProjectDiffResult { Status = ProjectDiffExecutionStatus.Success, @@ -106,7 +130,16 @@ public async Task GetProjectDiff( }; } - var fromGraph = await ProjectGraphFactory.BuildForGitTree( + + _logger.LogInformation("Found {NumChangedFiles} changed files", changedFiles.Count); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Found changed files: {ChangedFiles}", changedFiles); + } + + var projectGraphFactory = new ProjectGraphFactory(_loggerFactory); + + var fromGraph = await projectGraphFactory.BuildForGitTree( repo, baseCommit.Tree, entrypointProvider, @@ -116,14 +149,14 @@ public async Task GetProjectDiff( ProjectGraph toGraph; if (headCommit is null) { - toGraph = await ProjectGraphFactory.BuildForWorkingDirectory( + toGraph = await projectGraphFactory.BuildForWorkingDirectory( entrypointProvider, cancellationToken ); } else { - toGraph = await ProjectGraphFactory.BuildForGitTree( + toGraph = await projectGraphFactory.BuildForGitTree( repo, headCommit.Tree, entrypointProvider, diff --git a/src/ProjectDiff.Core/ProjectGraphFactory.cs b/src/ProjectDiff.Core/ProjectGraphFactory.cs index c3cbba8..615226a 100644 --- a/src/ProjectDiff.Core/ProjectGraphFactory.cs +++ b/src/ProjectDiff.Core/ProjectGraphFactory.cs @@ -5,32 +5,47 @@ using Microsoft.Build.Execution; using Microsoft.Build.FileSystem; using Microsoft.Build.Graph; +using Microsoft.Extensions.Logging; using ProjectDiff.Core.Entrypoints; namespace ProjectDiff.Core; -public static class ProjectGraphFactory +public sealed class ProjectGraphFactory { - public static async Task BuildForGitTree( + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; + + public ProjectGraphFactory(ILoggerFactory loggerFactory) + { + _loggerFactory = loggerFactory; + _logger = loggerFactory.CreateLogger(); + } + + public async Task BuildForGitTree( Repository repository, Tree tree, IEntrypointProvider entrypointProvider, CancellationToken cancellationToken = default ) { + _logger.LogInformation("Building project graph for Git tree '{TreeId}'", tree.Sha); using var projectCollection = new ProjectCollection(); var fs = new GitTreeFileSystem( repository, tree, projectCollection, - [] - ); - - fs.LazyLoadProjects = false; + [], + _loggerFactory.CreateLogger() + ) + { + // Disable eager loading of projects during entrypoint discovery to prevent user accidentally loading projects + EagerLoadProjects = false + }; var entrypoints = await entrypointProvider.GetEntrypoints(fs, cancellationToken); - fs.LazyLoadProjects = true; + // Enable eager loading to fix issue with ms build not using the provided file system to load imports + fs.EagerLoadProjects = true; var graph = new ProjectGraph( entrypoints, projectCollection, @@ -51,7 +66,7 @@ public static async Task BuildForGitTree( return graph; } - public static async Task BuildForWorkingDirectory( + public async Task BuildForWorkingDirectory( IEntrypointProvider solutionFile, CancellationToken cancellationToken = default ) diff --git a/src/ProjectDiff.Tool/Program.cs b/src/ProjectDiff.Tool/Program.cs index c75cfcc..9d1eb17 100644 --- a/src/ProjectDiff.Tool/Program.cs +++ b/src/ProjectDiff.Tool/Program.cs @@ -1,9 +1,8 @@ -using System.CommandLine; -using Microsoft.Build.Locator; +using Microsoft.Build.Locator; using ProjectDiff.Tool; MSBuildLocator.RegisterDefaults(); -var tool = ProjectDiffTool.Create(new SystemConsole()); +var tool = ProjectDiffTool.BuildCli(new SystemConsole()); return await tool.InvokeAsync(args); \ No newline at end of file diff --git a/src/ProjectDiff.Tool/ProjectDiff.Tool.csproj b/src/ProjectDiff.Tool/ProjectDiff.Tool.csproj index e3b99ae..8964fb9 100644 --- a/src/ProjectDiff.Tool/ProjectDiff.Tool.csproj +++ b/src/ProjectDiff.Tool/ProjectDiff.Tool.csproj @@ -16,6 +16,7 @@ all + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/ProjectDiff.Tool/ProjectDiffCommand.cs b/src/ProjectDiff.Tool/ProjectDiffCommand.cs index cc36524..36712d0 100644 --- a/src/ProjectDiff.Tool/ProjectDiffCommand.cs +++ b/src/ProjectDiff.Tool/ProjectDiffCommand.cs @@ -4,6 +4,7 @@ using System.Text.Json.Serialization; using Microsoft.Build.Construction; using Microsoft.Build.Evaluation; +using Microsoft.Extensions.Logging; using ProjectDiff.Core; using ProjectDiff.Core.Entrypoints; @@ -118,6 +119,12 @@ public sealed class ProjectDiffCommand : RootCommand "Ignore changes in specific files. If these files are a part of the build evaluation process they will still be evaluated, however these files will be considered unchanged by the diff process" }; + private static readonly Option LogLevelOption = new("--log-level") + { + DefaultValueFactory = _ => LogLevel.Information, + Description = "Set the log level for the command. Default is 'Information'.", + }; + private readonly IConsole _console; @@ -137,6 +144,7 @@ public ProjectDiffCommand(IConsole console) Options.Add(Format); Options.Add(OutputOption); Options.Add(IgnoreChangedFilesOption); + Options.Add(LogLevelOption); SetAction(ExecuteAsync); } @@ -155,7 +163,8 @@ private Task ExecuteAsync(ParseResult parseResult, CancellationToken cancel IncludeAdded = parseResult.GetValue(IncludeAdded), IncludeReferencing = parseResult.GetValue(IncludeReferencing), AbsolutePaths = parseResult.GetValue(AbsolutePaths), - IgnoreChangedFile = parseResult.GetRequiredValue(IgnoreChangedFilesOption) + IgnoreChangedFile = parseResult.GetRequiredValue(IgnoreChangedFilesOption), + LogLevel = parseResult.GetValue(LogLevelOption), }; return ExecuteCoreAsync(settings, cancellationToken); @@ -166,6 +175,14 @@ private async Task ExecuteCoreAsync( CancellationToken cancellationToken ) { + using var loggerFactory = LoggerFactory.Create(x => + { + x.AddConsole(c => c.LogToStandardErrorThreshold = LogLevel.Trace); // Log everything to stderr + x.SetMinimumLevel(settings.LogLevel); + } + ); + var logger = loggerFactory.CreateLogger(); + var diffOutput = new DiffOutput(settings.Output, _console); OutputFormat outputFormat; if (settings.Format is not null) @@ -174,24 +191,41 @@ CancellationToken cancellationToken } else if (settings.Output is not null) { + logger.LogDebug("Detecting output format from file extension {Extension}", settings.Output.Extension); outputFormat = GetOutputFormatFromExtension(settings.Output.Extension); + logger.LogDebug("Detected output format {Format}", outputFormat); } else { outputFormat = OutputFormat.Plain; } + if (outputFormat == OutputFormat.Slnf && settings.Solution is null) + { + logger.LogError("Cannot output as slnf format without solution file specified."); + return 1; + } + + logger.LogDebug("Using output format {Format}", outputFormat); + var executor = new ProjectDiffExecutor( new ProjectDiffExecutorOptions { FindMergeBase = settings.MergeBase, IgnoreChangedFiles = settings.IgnoreChangedFile, - } + }, + loggerFactory ); IEntrypointProvider entrypointProvider = settings.Solution is not null - ? new SolutionEntrypointProvider(settings.Solution) - : new DirectoryScanEntrypointProvider(_console.WorkingDirectory); + ? new SolutionEntrypointProvider( + settings.Solution, + loggerFactory.CreateLogger() + ) + : new DirectoryScanEntrypointProvider( + _console.WorkingDirectory, + loggerFactory.CreateLogger() + ); var result = await executor.GetProjectDiff( settings.Solution?.DirectoryName ?? _console.WorkingDirectory, @@ -203,35 +237,37 @@ CancellationToken cancellationToken if (result.Status != ProjectDiffExecutionStatus.Success) { - WriteError(_console, $"Failed to calculate project diff: {result.Status}"); + logger.LogError("Failed to calculate project diff '{Status}'", result.Status); return 1; } - var diff = result.Projects.Where(ShouldInclude) + var projects = result.Projects.Where(ShouldInclude) .OrderBy(it => it.ReferencedProjects.Count) - .ThenBy(it => it.Path); + .ThenBy(it => it.Path) + .ToList(); + + logger.LogInformation("Found {Count} projects in diff", projects.Count); + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogDebug("Diff projects: {Projects}", projects.Select(it => new { it.Path, it.Status, ReferencedProjects = string.Join(',', it.ReferencedProjects) })); + } + switch (outputFormat) { case OutputFormat.Plain: - await WritePlain(diffOutput, diff, settings.AbsolutePaths); + await WritePlain(diffOutput, projects, settings.AbsolutePaths); break; case OutputFormat.Json: - await WriteJson(diffOutput, diff, settings.AbsolutePaths); + await WriteJson(diffOutput, projects, settings.AbsolutePaths); break; case OutputFormat.Slnf: - if (settings.Solution is null) - { - WriteError(_console, "Cannot output slnf format without solution file specified."); - return 1; - } - - await WriteSlnf(diffOutput, settings.Solution, diff); + await WriteSlnf(diffOutput, settings.Solution!, projects); break; case OutputFormat.Traversal: - await WriteTraversal(diffOutput, diff, settings.AbsolutePaths); + await WriteTraversal(diffOutput, projects, settings.AbsolutePaths); break; default: - WriteError(_console, $"Unknown output format {settings.Format}"); + logger.LogError("Unknown output format {Format}", settings.Format); return 1; } @@ -352,11 +388,6 @@ bool absolutePaths element.Save(writer); } - private static void WriteError(IConsole console, string error) - { - console.Error.WriteLine(error); - } - private static OutputFormat GetOutputFormatFromExtension(string extension) => extension switch { ".slnf" => OutputFormat.Slnf, diff --git a/src/ProjectDiff.Tool/ProjectDiffSettings.cs b/src/ProjectDiff.Tool/ProjectDiffSettings.cs index 97f671b..b69050a 100644 --- a/src/ProjectDiff.Tool/ProjectDiffSettings.cs +++ b/src/ProjectDiff.Tool/ProjectDiffSettings.cs @@ -1,4 +1,6 @@ -namespace ProjectDiff.Tool; +using Microsoft.Extensions.Logging; + +namespace ProjectDiff.Tool; public sealed class ProjectDiffSettings { @@ -14,4 +16,5 @@ public sealed class ProjectDiffSettings public required OutputFormat? Format { get; init; } public required FileInfo? Output { get; init; } public required FileInfo[] IgnoreChangedFile { get; init; } = []; + public LogLevel LogLevel { get; init; } = LogLevel.Information; } \ No newline at end of file diff --git a/src/ProjectDiff.Tool/ProjectDiffTool.cs b/src/ProjectDiff.Tool/ProjectDiffTool.cs index 58d5e87..9b5a40f 100644 --- a/src/ProjectDiff.Tool/ProjectDiffTool.cs +++ b/src/ProjectDiff.Tool/ProjectDiffTool.cs @@ -2,29 +2,9 @@ namespace ProjectDiff.Tool; -public sealed class ProjectDiffTool +public static class ProjectDiffTool { - private readonly CommandLineConfiguration _cli; - - private ProjectDiffTool(CommandLineConfiguration cli) - { - _cli = cli; - } - - - public Task InvokeAsync(string[] args) - { - return _cli.InvokeAsync(args); - } - - - public static ProjectDiffTool Create(IConsole console) - { - var parser = BuildCli(console); - return new ProjectDiffTool(parser); - } - - private static CommandLineConfiguration BuildCli(IConsole console) + public static CommandLineConfiguration BuildCli(IConsole console) { return new CommandLineConfiguration(new ProjectDiffCommand(console)) { diff --git a/test/ProjectDiff.Tests/Core/BranchTests.cs b/test/ProjectDiff.Tests/Core/BranchTests.cs index 29df9b8..c6ffdff 100644 --- a/test/ProjectDiff.Tests/Core/BranchTests.cs +++ b/test/ProjectDiff.Tests/Core/BranchTests.cs @@ -1,4 +1,6 @@ -using ProjectDiff.Core; +using Microsoft.Extensions.Logging.Abstractions; +using ProjectDiff.Core; +using ProjectDiff.Core.Entrypoints; using ProjectDiff.Tests.Utils; namespace ProjectDiff.Tests.Core; @@ -20,13 +22,14 @@ public async Task AddProjectInNewBranch() "Sample/Sample.csproj", p => { p.AddProperty("TargetFrameworks", "net8.0;netstandard2.0"); } ); - await repo.WriteFileAsync("Sample/MyClass.cs", "// Some content"); + await repo.WriteAllTextAsync("Sample/MyClass.cs", "// Some content"); repo.StageAndCommitAllChanges(); - var executor = new ProjectDiffExecutor(new ProjectDiffExecutorOptions()); + var executor = new ProjectDiffExecutor(new ProjectDiffExecutorOptions(), NullLoggerFactory.Instance); var result = await executor.GetProjectDiff( - new FileInfo(sln), + repo.WorkingDirectory, + new SolutionEntrypointProvider(new FileInfo(sln), NullLogger.Instance), "master", "feature", TestContext.Current.CancellationToken @@ -49,7 +52,7 @@ public async Task RemoveProjectInNewBranch() "Sample/Sample.csproj", p => { p.AddProperty("TargetFrameworks", "net8.0;netstandard2.0"); } ); - await repo.WriteFileAsync("Sample/MyClass.cs", "// Some content"); + await repo.WriteAllTextAsync("Sample/MyClass.cs", "// Some content"); return (sln, project); } ); @@ -64,7 +67,8 @@ await repo.UpdateSolutionAsync( var executor = new ProjectDiffExecutor(new ProjectDiffExecutorOptions()); var result = await executor.GetProjectDiff( - new FileInfo(sln), + repo.WorkingDirectory, + new SolutionEntrypointProvider(new FileInfo(sln), NullLogger.Instance), "master", "feature", TestContext.Current.CancellationToken @@ -82,23 +86,26 @@ public async Task ModifyProjectInNewBranch() { using var res = await TestRepository.SetupAsync(static async repo => { - var sln = await repo.CreateSolutionAsync("Sample.sln", sln => sln.AddProject("Sample/Sample.csproj")); var project = repo.CreateProject( "Sample/Sample.csproj", p => { p.AddProperty("TargetFrameworks", "net8.0;netstandard2.0"); } ); - await repo.WriteFileAsync("Sample/MyClass.cs", "// Some content"); - return (sln, project); + await repo.WriteAllTextAsync("Sample/MyClass.cs", "// Some content"); + return project; } ); - var ((sln, project), repo) = res; + var (project, repo) = res; repo.CreateAndCheckoutBranch("feature"); - await repo.WriteFileAsync("Sample/MyClass.cs", "// Some new content"); + await repo.WriteAllTextAsync("Sample/MyClass.cs", "// Some new content"); repo.StageAndCommitAllChanges(); var executor = new ProjectDiffExecutor(new ProjectDiffExecutorOptions()); var result = await executor.GetProjectDiff( - new FileInfo(sln), + repo.WorkingDirectory, + new DirectoryScanEntrypointProvider( + repo.WorkingDirectory, + NullLogger.Instance + ), "master", "feature", TestContext.Current.CancellationToken @@ -113,25 +120,28 @@ public async Task ModifyProjectInNewBranch() [Fact] public async Task ModifyProjectInBaseBranch_WithNoMergeBaseOption() { - using var res = await TestRepository.SetupAsync(static async repo => + using var res = await TestRepository.SetupAsync(static repo => { - var sln = await repo.CreateSolutionAsync("Sample.sln", sln => sln.AddProject("Core/Core.csproj")); var project = repo.CreateProject( "Core/Core.csproj", p => { p.AddProperty("TargetFrameworks", "net8.0;netstandard2.0"); } ); - return (sln, project); + return Task.FromResult(project); } ); - var ((sln, project), repo) = res; + var (project, repo) = res; repo.CreateBranch("feature"); // Create the branch without checking it out - await repo.WriteFileAsync("Core/MyClass.cs", "// Some content"); + await repo.WriteAllTextAsync("Core/MyClass.cs", "// Some content"); repo.StageAndCommitAllChanges(); var executor = new ProjectDiffExecutor(new ProjectDiffExecutorOptions()); var result = await executor.GetProjectDiff( - new FileInfo(sln), + repo.WorkingDirectory, + new DirectoryScanEntrypointProvider( + repo.WorkingDirectory, + NullLogger.Instance + ), "master", "feature", TestContext.Current.CancellationToken @@ -147,20 +157,18 @@ public async Task ModifyProjectInBaseBranch_WithNoMergeBaseOption() [Fact] public async Task ModifyProjectInBaseBranch_WithMergeBaseOption() { - using var res = await TestRepository.SetupAsync(static async repo => + using var repo = await TestRepository.SetupAsync(static repo => { - var sln = await repo.CreateSolutionAsync("Sample.sln", sln => sln.AddProject("Core/Core.csproj")); - var project = repo.CreateProject( + repo.CreateProject( "Core/Core.csproj", p => { p.AddProperty("TargetFrameworks", "net8.0;netstandard2.0"); } ); - return (sln, project); + return Task.CompletedTask; } ); - var ((sln, project), repo) = res; repo.CreateBranch("feature"); // Create the branch without checking it out - await repo.WriteFileAsync("Core/MyClass.cs", "// Some content"); + await repo.WriteAllTextAsync("Core/MyClass.cs", "// Some content"); repo.StageAndCommitAllChanges(); var executor = new ProjectDiffExecutor( @@ -170,7 +178,11 @@ public async Task ModifyProjectInBaseBranch_WithMergeBaseOption() } ); var result = await executor.GetProjectDiff( - new FileInfo(sln), + repo.WorkingDirectory, + new DirectoryScanEntrypointProvider( + repo.WorkingDirectory, + NullLogger.Instance + ), "master", "feature", TestContext.Current.CancellationToken diff --git a/test/ProjectDiff.Tests/Core/ErrorTests.cs b/test/ProjectDiff.Tests/Core/ErrorTests.cs index 03c870b..9081aea 100644 --- a/test/ProjectDiff.Tests/Core/ErrorTests.cs +++ b/test/ProjectDiff.Tests/Core/ErrorTests.cs @@ -1,10 +1,35 @@ -using ProjectDiff.Core; +using Microsoft.Build.FileSystem; +using Microsoft.Build.Graph; +using ProjectDiff.Core; +using ProjectDiff.Core.Entrypoints; using ProjectDiff.Tests.Utils; namespace ProjectDiff.Tests.Core; public sealed class ErrorTests { + [Fact] + public async Task NonExistingRepositoryReturnsError() + { + var dir = Directory.CreateTempSubdirectory("dotnet-proj-diff-test"); + try + { + var executor = new ProjectDiffExecutor(new ProjectDiffExecutorOptions()); + + var result = await executor.GetProjectDiff( + dir.FullName, + new EmptyEntrypointProvider(), + cancellationToken: TestContext.Current.CancellationToken + ); + + Assert.Equal(ProjectDiffExecutionStatus.RepositoryNotFound, result.Status); + } + finally + { + dir.Delete(); + } + } + [Fact] public async Task InvalidBaseCommitReturnsError() { @@ -13,7 +38,8 @@ public async Task InvalidBaseCommitReturnsError() var executor = new ProjectDiffExecutor(new ProjectDiffExecutorOptions()); var result = await executor.GetProjectDiff( - new FileInfo("doesnotexist.sln"), + repo.WorkingDirectory, + new EmptyEntrypointProvider(), "SOME-INVALID-COMMIT-SHA", cancellationToken: TestContext.Current.CancellationToken ); @@ -29,7 +55,8 @@ public async Task InvalidHeadCommitReturnsError() var executor = new ProjectDiffExecutor(new ProjectDiffExecutorOptions()); var result = await executor.GetProjectDiff( - new FileInfo("doesnotexist.sln"), + repo.WorkingDirectory, + new EmptyEntrypointProvider(), "HEAD", "SOME-INVALID-COMMIT-SHA", TestContext.Current.CancellationToken @@ -37,4 +64,16 @@ public async Task InvalidHeadCommitReturnsError() Assert.Equal(ProjectDiffExecutionStatus.HeadCommitNotFound, result.Status); } + + + private sealed class EmptyEntrypointProvider : IEntrypointProvider + { + public Task> GetEntrypoints( + MSBuildFileSystemBase fs, + CancellationToken cancellationToken + ) + { + return Task.FromResult(Enumerable.Empty()); + } + } } \ No newline at end of file diff --git a/test/ProjectDiff.Tests/Core/GitTreeFileSystemTests.cs b/test/ProjectDiff.Tests/Core/GitTreeFileSystemTests.cs index a48c09e..b29212c 100644 --- a/test/ProjectDiff.Tests/Core/GitTreeFileSystemTests.cs +++ b/test/ProjectDiff.Tests/Core/GitTreeFileSystemTests.cs @@ -1,5 +1,6 @@ -using LibGit2Sharp; +using System.Text; using Microsoft.Build.Evaluation; +using Microsoft.Extensions.Logging.Abstractions; using ProjectDiff.Core; using ProjectDiff.Tests.Utils; @@ -8,14 +9,227 @@ namespace ProjectDiff.Tests.Core; public sealed class GitTreeFileSystemTests { [Fact] - public async Task EnumerateDirectoriesReturnsDirectories() + public async Task ReadFile_ReturnsCorrectContent() + { + using var repo = await TestRepository.SetupAsync(async repo => + await repo.WriteAllTextAsync("test.txt", "Hello, World!") + ); + + using var projects = new ProjectCollection(); + var fileSystem = new GitTreeFileSystem( + repo, + repo.HeadTree, + projects, + [], + NullLogger.Instance + ); + + using var reader = fileSystem.ReadFile(Path.Combine(repo.WorkingDirectory, "test.txt")); + var content = await reader.ReadToEndAsync(TestContext.Current.CancellationToken); + + Assert.Equal("Hello, World!", content); + } + + [Fact] + public async Task ReadFileAllText_ReturnsCorrectContent() + { + using var repo = await TestRepository.SetupAsync(async repo => + await repo.WriteAllTextAsync("test.txt", "Hello, World!") + ); + + using var projects = new ProjectCollection(); + var fileSystem = new GitTreeFileSystem( + repo, + repo.HeadTree, + projects, + [], + NullLogger.Instance + ); + + var content = fileSystem.ReadFileAllText(Path.Combine(repo.WorkingDirectory, "test.txt")); + + Assert.Equal("Hello, World!", content); + } + + [Fact] + public async Task ReadFileAllBytes_ReturnsCorrectContent() + { + var expectedBytes = "Hello, World!"u8.ToArray(); + + using var repo = await TestRepository.SetupAsync(async repo => + await repo.WriteAllTextAsync("test.txt", "Hello, World!") + ); + + using var projects = new ProjectCollection(); + var fileSystem = new GitTreeFileSystem( + repo, + repo.HeadTree, + projects, + [], + NullLogger.Instance + ); + + var bytes = fileSystem.ReadFileAllBytes(Path.Combine(repo.WorkingDirectory, "test.txt")); + + Assert.Equal(expectedBytes, bytes); + } + + [Fact] + public async Task DirectoryExists_ReturnsTrueForExistingDirectory() + { + using var repo = await TestRepository.SetupAsync(async repo => + { + repo.CreateDirectory("subdir"); + await repo.WriteAllTextAsync("subdir/.gitkeep", ""); + } + ); + + using var projects = new ProjectCollection(); + var fileSystem = new GitTreeFileSystem( + repo, + repo.HeadTree, + projects, + [], + NullLogger.Instance + ); + + var exists = fileSystem.DirectoryExists(Path.Combine(repo.WorkingDirectory, "subdir")); + + Assert.True(exists); + } + + [Fact] + public async Task DirectoryExists_ReturnsFalseForNonExistingDirectory() + { + using var repo = await TestRepository.SetupAsync(_ => Task.CompletedTask); + + using var projects = new ProjectCollection(); + var fileSystem = new GitTreeFileSystem( + repo, + repo.HeadTree, + projects, + [], + NullLogger.Instance + ); + + var exists = fileSystem.DirectoryExists(Path.Combine(repo.WorkingDirectory, "nonexistent")); + + Assert.False(exists); + } + + [Fact] + public async Task FileExists_ReturnsTrueForExistingFile() + { + using var repo = await TestRepository.SetupAsync(async repo => + await repo.WriteAllTextAsync("test.txt", "Hello, World!") + ); + + using var projects = new ProjectCollection(); + var fileSystem = new GitTreeFileSystem( + repo, + repo.HeadTree, + projects, + [], + NullLogger.Instance + ); + + var exists = fileSystem.FileExists(Path.Combine(repo.WorkingDirectory, "test.txt")); + + Assert.True(exists); + } + + [Fact] + public async Task FileExists_ReturnsFalseForNonExistingFile() + { + using var repo = await TestRepository.SetupAsync(_ => Task.CompletedTask); + + using var projects = new ProjectCollection(); + var fileSystem = new GitTreeFileSystem( + repo, + repo.HeadTree, + projects, + [], + NullLogger.Instance + ); + + var exists = fileSystem.FileExists(Path.Combine(repo.WorkingDirectory, "nonexistent.txt")); + + Assert.False(exists); + } + + [Fact] + public async Task FileOrDirectoryExists_ReturnsTrueForExistingFile() + { + using var repo = await TestRepository.SetupAsync(async repo => + await repo.WriteAllTextAsync("test.txt", "Hello, World!") + ); + + using var projects = new ProjectCollection(); + var fileSystem = new GitTreeFileSystem( + repo, + repo.HeadTree, + projects, + [], + NullLogger.Instance + ); + + var exists = fileSystem.FileOrDirectoryExists(Path.Combine(repo.WorkingDirectory, "test.txt")); + + Assert.True(exists); + } + + [Fact] + public async Task FileOrDirectoryExists_ReturnsTrueForExistingDirectory() + { + using var repo = await TestRepository.SetupAsync(async repo => + { + repo.CreateDirectory("subdir"); + await repo.WriteAllTextAsync("subdir/.gitkeep", ""); + } + ); + + using var projects = new ProjectCollection(); + var fileSystem = new GitTreeFileSystem( + repo, + repo.HeadTree, + projects, + [], + NullLogger.Instance + ); + + var exists = fileSystem.FileOrDirectoryExists(Path.Combine(repo.WorkingDirectory, "subdir")); + + Assert.True(exists); + } + + [Fact] + public async Task FileOrDirectoryExists_ReturnsFalseForNonExistingPath() + { + using var repo = await TestRepository.SetupAsync(_ => Task.CompletedTask); + + using var projects = new ProjectCollection(); + var fileSystem = new GitTreeFileSystem( + repo, + repo.HeadTree, + projects, + [], + NullLogger.Instance + ); + + var exists = fileSystem.FileOrDirectoryExists(Path.Combine(repo.WorkingDirectory, "nonexistent")); + + Assert.False(exists); + } + + [Fact] + public async Task EnumerateDirectories_ReturnsDirectories() { using var repo = await TestRepository.SetupAsync(async repo => { repo.CreateDirectory("subdir1"); repo.CreateDirectory("subdir2"); - await repo.WriteFileAsync("subdir1/test.txt", "Hello, World!"); - await repo.WriteFileAsync("subdir2/another.txt", "Another file"); + await repo.WriteAllTextAsync("subdir1/test.txt", "Hello, World!"); + await repo.WriteAllTextAsync("subdir2/another.txt", "Another file"); } ); @@ -24,7 +238,8 @@ public async Task EnumerateDirectoriesReturnsDirectories() repo, repo.HeadTree, projects, - [] + [], + NullLogger.Instance ); var directories = fileSystem.EnumerateDirectories( @@ -38,19 +253,19 @@ public async Task EnumerateDirectoriesReturnsDirectories() Path.Combine(repo.WorkingDirectory, "subdir1"), Path.Combine(repo.WorkingDirectory, "subdir2") }; - + Assert.Equivalent(expectedDirectories, directories); } - + [Fact] - public async Task EnumerateDirectoriesInSubdirectoryReturnsSubdirectories() + public async Task EnumerateDirectories_InSubdirectoryReturnsSubdirectories() { using var repo = await TestRepository.SetupAsync(async repo => { repo.CreateDirectory("subdir"); repo.CreateDirectory("subdir/nested"); - await repo.WriteFileAsync("subdir/test.txt", "Hello, World!"); - await repo.WriteFileAsync("subdir/nested/nested.txt", "Nested file"); + await repo.WriteAllTextAsync("subdir/test.txt", "Hello, World!"); + await repo.WriteAllTextAsync("subdir/nested/nested.txt", "Nested file"); } ); @@ -59,7 +274,8 @@ public async Task EnumerateDirectoriesInSubdirectoryReturnsSubdirectories() repo, repo.HeadTree, projects, - [] + [], + NullLogger.Instance ); var directories = fileSystem.EnumerateDirectories( @@ -72,12 +288,12 @@ public async Task EnumerateDirectoriesInSubdirectoryReturnsSubdirectories() { Path.Combine(repo.WorkingDirectory, "subdir", "nested") }; - + Assert.Equivalent(expectedDirectories, directories); } - + [Fact] - public async Task EnumerateDirectoriesWithSearchOptionAllDirectoriesReturnsAllDirectories() + public async Task EnumerateDirectories_WithSearchOptionAllDirectoriesReturnsAllDirectories() { using var repo = await TestRepository.SetupAsync(async repo => { @@ -85,10 +301,10 @@ public async Task EnumerateDirectoriesWithSearchOptionAllDirectoriesReturnsAllDi repo.CreateDirectory("subdir2"); repo.CreateDirectory("subdir1/nested"); repo.CreateDirectory("subdir2/another"); - await repo.WriteFileAsync("subdir1/test.txt", "Hello, World!"); - await repo.WriteFileAsync("subdir2/another/another.txt", "Another file"); - await repo.WriteFileAsync("subdir1/nested/nested.txt", "Nested file"); - await repo.WriteFileAsync("test.txt", "Root file"); + await repo.WriteAllTextAsync("subdir1/test.txt", "Hello, World!"); + await repo.WriteAllTextAsync("subdir2/another/another.txt", "Another file"); + await repo.WriteAllTextAsync("subdir1/nested/nested.txt", "Nested file"); + await repo.WriteAllTextAsync("test.txt", "Root file"); } ); @@ -97,7 +313,8 @@ public async Task EnumerateDirectoriesWithSearchOptionAllDirectoriesReturnsAllDi repo, repo.HeadTree, projects, - [] + [], + NullLogger.Instance ); var directories = fileSystem.EnumerateDirectories( @@ -106,16 +323,17 @@ public async Task EnumerateDirectoriesWithSearchOptionAllDirectoriesReturnsAllDi SearchOption.AllDirectories ).ToList(); - string[] expectedDirectories = [ + string[] expectedDirectories = + [ Path.Combine(repo.WorkingDirectory, "subdir1"), Path.Combine(repo.WorkingDirectory, "subdir1", "nested"), Path.Combine(repo.WorkingDirectory, "subdir2"), Path.Combine(repo.WorkingDirectory, "subdir2", "another") ]; - + Assert.Equivalent(expectedDirectories, directories); } - + [Fact] public void EnumerateFilesInEmptyDirectoryReturnsEmpty() { @@ -126,7 +344,8 @@ public void EnumerateFilesInEmptyDirectoryReturnsEmpty() repo, repo.HeadTree, projects, - [] + [], + NullLogger.Instance ); var files = fileSystem.EnumerateFiles( @@ -139,11 +358,11 @@ public void EnumerateFilesInEmptyDirectoryReturnsEmpty() } [Fact] - public async Task EnumerateFilesInDirectoryWithFileReturnsFile() + public async Task EnumerateFiles_InDirectoryWithFileReturnsFile() { using var repo = await TestRepository.SetupAsync(async repo => { - await repo.WriteFileAsync("test.txt", "Hello, World!"); + await repo.WriteAllTextAsync("test.txt", "Hello, World!"); } ); @@ -152,7 +371,8 @@ public async Task EnumerateFilesInDirectoryWithFileReturnsFile() repo, repo.HeadTree, projects, - [] + [], + NullLogger.Instance ); var files = fileSystem.EnumerateFiles( @@ -170,19 +390,19 @@ public async Task EnumerateFilesInDirectoryWithFileReturnsFile() } [Fact] - public async Task EnumerateFilesInDirectoryWithSubdirectoryReturnsFiles() + public async Task EnumerateFiles_InDirectoryWithSubdirectoryReturnsFiles() { using var repo = await TestRepository.SetupAsync(async repo => { repo.CreateDirectory("subdir"); - await repo.WriteFileAsync("subdir/test.txt", "Hello, World!"); - await repo.WriteFileAsync("subdir/another.txt", "Another file"); + await repo.WriteAllTextAsync("subdir/test.txt", "Hello, World!"); + await repo.WriteAllTextAsync("subdir/another.txt", "Another file"); // Should not be included - await repo.WriteFileAsync("test.txt", "Root file"); - + await repo.WriteAllTextAsync("test.txt", "Root file"); + repo.CreateDirectory("subdir/nested"); - await repo.WriteFileAsync("subdir/nested/nested.txt", "Nested file"); + await repo.WriteAllTextAsync("subdir/nested/nested.txt", "Nested file"); } ); @@ -191,7 +411,8 @@ public async Task EnumerateFilesInDirectoryWithSubdirectoryReturnsFiles() repo, repo.HeadTree, projects, - [] + [], + NullLogger.Instance ); var files = fileSystem.EnumerateFiles( @@ -210,14 +431,14 @@ public async Task EnumerateFilesInDirectoryWithSubdirectoryReturnsFiles() } [Fact] - public async Task EnumerateFilesWithSearchOptionAllDirectoriesReturnsAllFiles() + public async Task EnumerateFiles_WithSearchOptionAllDirectoriesReturnsAllFiles() { using var repo = await TestRepository.SetupAsync(async repo => { repo.CreateDirectory("subdir"); - await repo.WriteFileAsync("subdir/test.txt", "Hello, World!"); - await repo.WriteFileAsync("subdir/another.txt", "Another file"); - await repo.WriteFileAsync("test.txt", "Root file"); + await repo.WriteAllTextAsync("subdir/test.txt", "Hello, World!"); + await repo.WriteAllTextAsync("subdir/another.txt", "Another file"); + await repo.WriteAllTextAsync("test.txt", "Root file"); } ); @@ -226,7 +447,8 @@ public async Task EnumerateFilesWithSearchOptionAllDirectoriesReturnsAllFiles() repo, repo.HeadTree, projects, - [] + [], + NullLogger.Instance ); var files = fileSystem.EnumerateFiles( @@ -242,20 +464,20 @@ public async Task EnumerateFilesWithSearchOptionAllDirectoriesReturnsAllFiles() Path.Combine(repo.WorkingDirectory, "test.txt") ]; - + Assert.Equivalent(expectedFiles, files); } [Fact] - public async Task EnumerateFilesInSubdirectoryWithSearchOptionAllDirectoriesReturnsFilesInSubdirectory() + public async Task EnumerateFiles_InSubdirectoryWithSearchOptionAllDirectoriesReturnsFilesInSubdirectory() { using var repo = await TestRepository.SetupAsync(async repo => { repo.CreateDirectory("subdir"); - await repo.WriteFileAsync("subdir/test.txt", "Hello, World!"); - await repo.WriteFileAsync("subdir/another.txt", "Another file"); - await repo.WriteFileAsync("test.txt", "Root file"); + await repo.WriteAllTextAsync("subdir/test.txt", "Hello, World!"); + await repo.WriteAllTextAsync("subdir/another.txt", "Another file"); + await repo.WriteAllTextAsync("test.txt", "Root file"); } ); @@ -264,7 +486,8 @@ public async Task EnumerateFilesInSubdirectoryWithSearchOptionAllDirectoriesRetu repo, repo.HeadTree, projects, - [] + [], + NullLogger.Instance ); var files = fileSystem.EnumerateFiles( diff --git a/test/ProjectDiff.Tests/Core/IgnoreChangesTests.cs b/test/ProjectDiff.Tests/Core/IgnoreChangesTests.cs index 8282536..6bc1462 100644 --- a/test/ProjectDiff.Tests/Core/IgnoreChangesTests.cs +++ b/test/ProjectDiff.Tests/Core/IgnoreChangesTests.cs @@ -1,4 +1,6 @@ -using ProjectDiff.Core; +using Microsoft.Extensions.Logging.Abstractions; +using ProjectDiff.Core; +using ProjectDiff.Core.Entrypoints; using ProjectDiff.Tests.Utils; namespace ProjectDiff.Tests.Core; @@ -8,24 +10,16 @@ public sealed class IgnoreChangesTests [Fact] public async Task IgnoresModifiedFiles() { - using var res = await TestRepository.SetupAsync(async r => + using var repo = await TestRepository.SetupAsync(async r => { r.CreateDirectory("Core"); r.CreateProject("Core/Core.csproj"); - r.WriteAllText("Core/Sample.cs", "// Some file here"); - - return await r.CreateSolutionAsync( - "MySln.sln", - model => { model.AddProject("Core/Core.csproj"); } - ); + await r.WriteAllTextAsync("Core/Sample.cs", "// Some file here"); } ); - - var (sln, repo) = res; - - repo.WriteAllText("Core/Sample.cs", "// New content here"); + await repo.WriteAllTextAsync("Core/Sample.cs", "// New content here"); var executor = new ProjectDiffExecutor( new ProjectDiffExecutorOptions @@ -34,7 +28,11 @@ public async Task IgnoresModifiedFiles() } ); var diff = await executor.GetProjectDiff( - new FileInfo(sln), + repo.WorkingDirectory, + new DirectoryScanEntrypointProvider( + repo.WorkingDirectory, + NullLogger.Instance + ), cancellationToken: TestContext.Current.CancellationToken ); Assert.Equal(ProjectDiffExecutionStatus.Success, diff.Status); @@ -47,23 +45,17 @@ public async Task IgnoresModifiedFiles() [Fact] public async Task IgnoresAddedFiles() { - using var res = await TestRepository.SetupAsync(async r => + using var repo = await TestRepository.SetupAsync(r => { r.CreateDirectory("Core"); r.CreateProject("Core/Core.csproj"); - - return await r.CreateSolutionAsync( - "MySln.sln", - model => { model.AddProject("Core/Core.csproj"); } - ); + return Task.CompletedTask; } ); - var (sln, repo) = res; - - repo.WriteAllText("Core/MyClass.cs", "// Some content here"); - repo.WriteAllText("README.md", "Hello there"); // + await repo.WriteAllTextAsync("Core/MyClass.cs", "// Some content here"); + await repo.WriteAllTextAsync("README.md", "Hello there"); // var executor = new ProjectDiffExecutor( new ProjectDiffExecutorOptions { @@ -72,7 +64,11 @@ public async Task IgnoresAddedFiles() ); var diff = await executor.GetProjectDiff( - new FileInfo(sln), + repo.WorkingDirectory, + new DirectoryScanEntrypointProvider( + repo.WorkingDirectory, + NullLogger.Instance + ), cancellationToken: TestContext.Current.CancellationToken ); Assert.Equal(ProjectDiffExecutionStatus.Success, diff.Status); @@ -88,20 +84,14 @@ public async Task IgnoresAddedFiles() [Fact] public async Task IgnoresDeletedFiles() { - using var res = await TestRepository.SetupAsync(async r => + using var repo = await TestRepository.SetupAsync(async r => { r.CreateDirectory("Core"); r.CreateProject("Core/Core.csproj"); - r.WriteAllText("Core/Sample.cs", "// Some file here"); - - return await r.CreateSolutionAsync( - "MySln.sln", - model => { model.AddProject("Core/Core.csproj"); } - ); + await r.WriteAllTextAsync("Core/Sample.cs", "// Some file here"); } ); - var (sln, repo) = res; repo.DeleteFile("Core/Sample.cs"); var executor = new ProjectDiffExecutor( @@ -111,7 +101,11 @@ public async Task IgnoresDeletedFiles() } ); var diff = await executor.GetProjectDiff( - new FileInfo(sln), + repo.WorkingDirectory, + new DirectoryScanEntrypointProvider( + repo.WorkingDirectory, + NullLogger.Instance + ), cancellationToken: TestContext.Current.CancellationToken ); Assert.Equal(ProjectDiffExecutionStatus.Success, diff.Status); diff --git a/test/ProjectDiff.Tests/Core/MultiFrameworkTests.cs b/test/ProjectDiff.Tests/Core/MultiFrameworkTests.cs index b7af766..1e0946f 100644 --- a/test/ProjectDiff.Tests/Core/MultiFrameworkTests.cs +++ b/test/ProjectDiff.Tests/Core/MultiFrameworkTests.cs @@ -1,4 +1,6 @@ -using ProjectDiff.Core; +using Microsoft.Extensions.Logging.Abstractions; +using ProjectDiff.Core; +using ProjectDiff.Core.Entrypoints; using ProjectDiff.Tests.Utils; namespace ProjectDiff.Tests.Core; @@ -10,19 +12,16 @@ public static async Task FileModifiedInMultiFrameworkProjectOnlyReturnsASinglePr { using var res = await TestRepository.SetupAsync(static async r => { - var sln = await r.CreateSolutionAsync("Sample.sln", sln => sln.AddProject("Sample/Sample.csproj")); r.CreateDirectory("Sample"); - var project = r.CreateProject( + await r.WriteAllTextAsync("Sample/MyClass.cs", "// Some content"); + return r.CreateProject( "Sample/Sample.csproj", p => { p.AddProperty("TargetFrameworks", "net9.0;net8.0;netstandard2.0"); } ); - await r.WriteFileAsync("Sample/MyClass.cs", "// Some content"); - - return (sln, project); } ); - var ((sln, project), repo) = res; - await repo.WriteFileAsync("Sample/MyClass.cs", "// Some new content"); + var (project, repo) = res; + await repo.WriteAllTextAsync("Sample/MyClass.cs", "// Some new content"); var executor = new ProjectDiffExecutor( new ProjectDiffExecutorOptions { @@ -30,8 +29,11 @@ public static async Task FileModifiedInMultiFrameworkProjectOnlyReturnsASinglePr } ); var result = await executor.GetProjectDiff( - new FileInfo(sln), - "HEAD", + repo.WorkingDirectory, + new DirectoryScanEntrypointProvider( + repo.WorkingDirectory, + NullLogger.Instance + ), cancellationToken: TestContext.Current.CancellationToken ); diff --git a/test/ProjectDiff.Tests/ProjectDiff.Tests.csproj b/test/ProjectDiff.Tests/ProjectDiff.Tests.csproj index 3740659..2c8e7a8 100644 --- a/test/ProjectDiff.Tests/ProjectDiff.Tests.csproj +++ b/test/ProjectDiff.Tests/ProjectDiff.Tests.csproj @@ -11,14 +11,14 @@ - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/test/ProjectDiff.Tests/Tool/ProjectDiffTests.cs b/test/ProjectDiff.Tests/Tool/ProjectDiffTests.cs index 240109a..3671179 100644 --- a/test/ProjectDiff.Tests/Tool/ProjectDiffTests.cs +++ b/test/ProjectDiff.Tests/Tool/ProjectDiffTests.cs @@ -32,7 +32,7 @@ public async Task DetectsAddedFiles(OutputFormat format) ); r.CreateDirectory("Sample"); - await r.WriteFileAsync("Sample/MyClass.cs", "// Some content"); + await r.WriteAllTextAsync("Sample/MyClass.cs", "// Some content"); r.CreateProject("Sample/Sample.csproj"); return solution; @@ -40,7 +40,7 @@ public async Task DetectsAddedFiles(OutputFormat format) ); var (sln, repo) = res; - await repo.WriteFileAsync("Sample/MyClass2", "// Some other content"); + await repo.WriteAllTextAsync("Sample/MyClass2", "// Some other content"); var output = await ExecuteAndReadStdout(repo, sln, $"--format={format}"); @@ -59,7 +59,7 @@ public async Task DetectsDeletedFiles(OutputFormat format) r.CreateDirectory("Sample"); r.CreateProject("Sample/Sample.csproj"); - await r.WriteFileAsync("Sample/MyClass.cs", "// Some content"); + await r.WriteAllTextAsync("Sample/MyClass.cs", "// Some content"); return sln; } @@ -83,13 +83,13 @@ public async Task DetectsModifiedFiles(OutputFormat format) var sln = await r.CreateSolutionAsync("Sample.sln", sln => sln.AddProject("Sample/Sample.csproj")); r.CreateDirectory("Sample"); r.CreateProject("Sample/Sample.csproj"); - await r.WriteFileAsync("Sample/MyClass.cs", "// Some content"); + await r.WriteAllTextAsync("Sample/MyClass.cs", "// Some content"); return sln; } ); var (sln, repo) = res; - await repo.WriteFileAsync("Sample/MyClass.cs", "// Some new content"); + await repo.WriteAllTextAsync("Sample/MyClass.cs", "// Some new content"); var output = await ExecuteAndReadStdout(repo, sln, $"--format={format}"); @@ -126,7 +126,7 @@ public async Task DetectsChangesInReferencedProjects(OutputFormat format) ); var (sln, repo) = res; - await repo.WriteFileAsync("Sample/MyClass.cs", "// Some new content"); + await repo.WriteAllTextAsync("Sample/MyClass.cs", "// Some new content"); var output = await ExecuteAndReadStdout(repo, sln, $"--format={format}"); @@ -170,7 +170,7 @@ public async Task DetectsChangesInNestedReferencedProjects(OutputFormat format) ); var (sln, repo) = res; - await repo.WriteFileAsync("Sample/MyClass.cs", "// Some new content"); + await repo.WriteAllTextAsync("Sample/MyClass.cs", "// Some new content"); var output = await ExecuteAndReadStdout(repo, sln, $"--format={format}"); @@ -274,7 +274,7 @@ public async Task DetectsAddedProjects(OutputFormat format) var sln = await r.CreateSolutionAsync("Sample.sln", sln => sln.AddProject("Sample/Sample.csproj")); r.CreateDirectory("Sample"); r.CreateProject("Sample/Sample.csproj"); - await r.WriteFileAsync("Sample/MyClass.cs", "// Some content"); + await r.WriteAllTextAsync("Sample/MyClass.cs", "// Some content"); return sln; } @@ -304,7 +304,7 @@ public async Task DetectsAddedProjectsWithDirectoryScan(OutputFormat format) { r.CreateDirectory("Sample"); r.CreateProject("Sample/Sample.csproj"); - await r.WriteFileAsync("Sample/MyClass.cs", "// Some content"); + await r.WriteAllTextAsync("Sample/MyClass.cs", "// Some content"); } ); @@ -318,6 +318,14 @@ await Verify(output, GetExtension(format)) .UseParameters(format); } + [Fact] + public void BuildingCliIsValid() + { + var console = new TestConsole(Directory.GetCurrentDirectory()); + var cli = ProjectDiffTool.BuildCli(console); + cli.ThrowIfInvalid(); + } + private static async Task ExecuteAndReadStdout( TestRepository repository, @@ -325,8 +333,9 @@ params string[] args ) { var console = new TestConsole(repository.WorkingDirectory); - var tool = ProjectDiffTool.Create(console); - var exitCode = await tool.InvokeAsync(args); + + var cli = ProjectDiffTool.BuildCli(console); + var exitCode = await cli.InvokeAsync(args.Append("--log-level=Debug").ToArray()); if (exitCode != 0) { var stderr = console.GetStandardError(); diff --git a/test/ProjectDiff.Tests/Utils/TestRepository.cs b/test/ProjectDiff.Tests/Utils/TestRepository.cs index 6ff6b0a..b739ffc 100644 --- a/test/ProjectDiff.Tests/Utils/TestRepository.cs +++ b/test/ProjectDiff.Tests/Utils/TestRepository.cs @@ -57,15 +57,7 @@ public void DeleteDirectory(string path, bool recursive = false) Directory.Delete(GetPath(path), recursive); } - public void WriteAllText(string file, string content) - { - File.WriteAllText( - GetPath(file), - content - ); - } - - public Task WriteFileAsync(string file, string content) + public Task WriteAllTextAsync(string file, string content) { return File.WriteAllTextAsync(GetPath(file), content); }