diff --git a/.ai/codebase.ps1 b/.ai/codebase.ps1 index bc28f4e..5352427 100644 --- a/.ai/codebase.ps1 +++ b/.ai/codebase.ps1 @@ -2,12 +2,28 @@ # This script creates a comprehensive text file containing the directory structure # and all source code files from your project's source directory for AI processing. # -# Customize the $sourceDirectory path to match your project's structure. +# You can target a specific subfolder with -SearchDirectory (relative to repo root) +# or provide an absolute path. + +param( + [string]$SearchDirectory = 'src' +) $repoRoot = git rev-parse --show-toplevel Write-Host "Repository root: $repoRoot" -$sourceDirectory = Join-Path $repoRoot 'src' +$sourceDirectory = if ([System.IO.Path]::IsPathRooted($SearchDirectory)) { + $SearchDirectory +} +else { + Join-Path $repoRoot $SearchDirectory +} + +if (-not (Test-Path -Path $sourceDirectory -PathType Container)) { + throw "Search directory does not exist or is not a folder: $sourceDirectory" +} + +$sourceDirectory = (Resolve-Path $sourceDirectory).Path Write-Host "Source directory: $sourceDirectory" $outputDir = "$repoRoot/.ai/outputs" @@ -35,20 +51,22 @@ Set-Content -Path $outputPath -Value $contextBlock # Extension -> language mapping $languageMap = @{ - '.cs' = 'csharp' - '.ps1' = 'powershell' - '.json' = 'json' - '.xml' = 'xml' - '.yml' = 'yaml' - '.yaml' = 'yaml' - '.md' = 'markdown' - '.sh' = 'bash' - '.ts' = 'typescript' - '.js' = 'javascript' + '.cs' = 'csharp' + '.razor' = 'razor' + '.ps1' = 'powershell' + '.json' = 'json' + '.xml' = 'xml' + '.yml' = 'yaml' + '.yaml' = 'yaml' + '.md' = 'markdown' + '.sh' = 'bash' + '.ts' = 'typescript' + '.js' = 'javascript' } +$keys = $languageMap.Keys | ForEach-Object { "*$_" } # Grab all files, filtering out the excluded directories -$allFiles = Get-ChildItem -Path $sourceDirectory -Recurse -File -Include *.cs, *.ps1, *.json, *.xml, *.yml, *.yaml, *.md, *.sh, *.ts, *.js | Where-Object { +$allFiles = Get-ChildItem -Path $sourceDirectory -Recurse -File -Include $keys | Where-Object { $_.FullName -notmatch $excludePattern } diff --git a/README.md b/README.md index e3563a6..410fac6 100644 --- a/README.md +++ b/README.md @@ -19,25 +19,49 @@ dotnet loom --help Use `init` to generate config and workflow files -``` +```bash dotnet loom init --force ``` -Invoke pipeline by passing target as arg +## Subcommands -``` +Loom provides subcommands for each build stage. You can run `dotnet loom [command] --help` for specific options. + +### Test + +Run your test suite: + +```bash dotnet loom test ``` -configure loom.json to publish/pack artifacts +### Build & Publish -``` +Configure `loom.json` to define artifacts, then build or publish them: + +```bash +dotnet loom build dotnet loom publish ``` -artifacts are not cleaned unless explicitly set +### Clean & Fresh Runs +Manual clean: + +```bash +dotnet loom clean +``` + +Prepend the `Clean` module to any pipeline run using the `--fresh` flag: + +```bash +dotnet loom release --fresh ``` -dotnet loom clean # The Clean module -dotnet release --clean # Prepends the Clean module to pipeline + +### Global Options + +Most commands support standard overrides: + +```bash +dotnet loom build --rid win-x64 ``` diff --git a/src/Loom.Build.Tests/Unit/BuildModuleTests.cs b/src/Loom.Build.Tests/Unit/BuildModuleTests.cs index 36c51f1..e333f47 100644 --- a/src/Loom.Build.Tests/Unit/BuildModuleTests.cs +++ b/src/Loom.Build.Tests/Unit/BuildModuleTests.cs @@ -1,26 +1,18 @@ using Loom.Config; using Loom.Modules; -using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; -using ModularPipelines; -using ModularPipelines.Context; + using ModularPipelines.DotNet.Options; using ModularPipelines.DotNet.Services; -using ModularPipelines.Models; using ModularPipelines.Options; + using Moq; namespace Loom.Build.Tests.Unit; public class BuildModuleTests { - private static string CreateTemporaryDirectory() - { - var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - Directory.CreateDirectory(tempDirectory); - return tempDirectory; - } - private static LoomSettings CreateSettings( BuildTarget target = BuildTarget.Build, string? configuration = null @@ -37,185 +29,172 @@ private static LoomSettings CreateSettings( }; } - private static PipelineBuilder CreateSilentPipelineBuilder( - LoomSettings settings, - string tempDir, - Mock mockDotNet - ) - { - var builder = Pipeline.CreateBuilder(); - builder.Services.AddSingleton(new LoomContext(settings, tempDir)); - builder.Services.AddSingleton(new ConfigurationBuilder().Build()); - builder.Services.AddSingleton(mockDotNet.Object); - builder.Services.AddModule(); - - builder.Options.PrintLogo = false; - builder.Options.ShowProgressInConsole = false; - builder.Options.PrintResults = false; - builder.Options.PrintDependencyChains = false; - builder.Options.DefaultLoggingOptions = CommandLoggingOptions.Silent; - - return builder; - } - [Test] public async Task ExecuteAsync_PassesFixedArgumentsAndSolution() { - var tempDir = CreateTemporaryDirectory(); - try - { - var settings = CreateSettings(); - var mockDotNet = new Mock(); - - var capturedOptions = new List(); - var capturedExecOptions = new List(); - - var emptyCommandResult = new CommandResult( - "", - "", - "", - "", - new Dictionary(), - DateTimeOffset.UtcNow, - DateTimeOffset.UtcNow, - TimeSpan.Zero, - 0 - ); - - mockDotNet - .Setup(d => - d.Build( - It.IsAny(), - It.IsAny(), - It.IsAny() - ) - ) - .Callback( - (opts, execOpts, _) => - { - capturedOptions.Add(opts); - capturedExecOptions.Add(execOpts); - } - ) - .ReturnsAsync(emptyCommandResult); + using var tempDir = new TempDirectory(); + var settings = CreateSettings(); + var mockDotNet = new Mock(); - var builder = CreateSilentPipelineBuilder(settings, tempDir, mockDotNet); - var pipeline = await builder.BuildAsync(); - await pipeline.RunAsync(); + var capturedOptions = new List(); + var capturedExecOptions = new List(); - await Assert.That(capturedOptions).Count().IsEqualTo(1); - await Assert.That(capturedOptions[0].ProjectSolution).IsEqualTo("test.sln"); - await Assert.That(capturedOptions[0].NoRestore).IsTrue(); - await Assert.That(capturedExecOptions).Count().IsEqualTo(1); - await Assert.That(capturedExecOptions[0].WorkingDirectory).IsEqualTo(tempDir); - } - finally - { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + + mockDotNet + .Setup(d => + d.Build( + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .Callback( + (opts, execOpts, _) => + { + capturedOptions.Add(opts); + capturedExecOptions.Add(execOpts); + } + ) + .ReturnsAsync(TestHelpers.EmptyCommandResult()); + + var builder = TestHelpers.CreateSilentPipelineBuilder(new LoomContext(settings, tempDir), + services => + { + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + services.AddModule(); + }); + var pipeline = await builder.BuildAsync(); + await pipeline.RunAsync(); + + await Assert.That(capturedOptions).Count().IsEqualTo(1); + await Assert.That(capturedOptions[0].ProjectSolution).IsEqualTo("test.sln"); + await Assert.That(capturedOptions[0].NoRestore).IsTrue(); + + await Assert.That(capturedExecOptions).Count().IsEqualTo(1); + await Assert.That(capturedExecOptions[0].WorkingDirectory).IsEqualTo(tempDir.Path); } [Test] public async Task ExecuteAsync_UsesReleaseConfiguration_WhenTargetIsPublishOrRelease() { - var tempDir = CreateTemporaryDirectory(); - try - { - var settings = CreateSettings(target: BuildTarget.Publish); - var mockDotNet = new Mock(); - - var capturedOptions = new List(); - var emptyCommandResult = new CommandResult( - "", - "", - "", - "", - new Dictionary(), - DateTimeOffset.UtcNow, - DateTimeOffset.UtcNow, - TimeSpan.Zero, - 0 - ); - - mockDotNet - .Setup(d => - d.Build( - It.IsAny(), - It.IsAny(), - It.IsAny() - ) - ) - .Callback( - (opts, execOpts, _) => - { - capturedOptions.Add(opts); - } - ) - .ReturnsAsync(emptyCommandResult); + using var tempDir = new TempDirectory(); + var settings = CreateSettings(target: BuildTarget.Publish); + var mockDotNet = new Mock(); - var builder = CreateSilentPipelineBuilder(settings, tempDir, mockDotNet); - var pipeline = await builder.BuildAsync(); - await pipeline.RunAsync(); + var capturedOptions = new List(); - await Assert.That(capturedOptions[0].Configuration).IsEqualTo("Release"); - } - finally - { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + + mockDotNet + .Setup(d => + d.Build( + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .Callback( + (opts, _, _) => + { + capturedOptions.Add(opts); + } + ) + .ReturnsAsync(TestHelpers.EmptyCommandResult()); + + var builder = TestHelpers.CreateSilentPipelineBuilder(new LoomContext(settings, tempDir), + services => + { + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + services.AddModule(); + }); + var pipeline = await builder.BuildAsync(); + await pipeline.RunAsync(); + + await Assert.That(capturedOptions[0].Configuration).IsEqualTo("Release"); } [Test] public async Task ExecuteAsync_UsesDebugConfiguration_WhenTargetIsDefault() { - var tempDir = CreateTemporaryDirectory(); - try - { - var settings = CreateSettings(); // Defaults to Build - var mockDotNet = new Mock(); - - var capturedOptions = new List(); - var emptyCommandResult = new CommandResult( - "", - "", - "", - "", - new Dictionary(), - DateTimeOffset.UtcNow, - DateTimeOffset.UtcNow, - TimeSpan.Zero, - 0 - ); - - mockDotNet - .Setup(d => - d.Build( - It.IsAny(), - It.IsAny(), - It.IsAny() - ) - ) - .Callback( - (opts, execOpts, _) => - { - capturedOptions.Add(opts); - } + using var tempDir = new TempDirectory(); + var settings = CreateSettings(); // Defaults to Build + var mockDotNet = new Mock(); + + var capturedOptions = new List(); + + + mockDotNet + .Setup(d => + d.Build( + It.IsAny(), + It.IsAny(), + It.IsAny() ) - .ReturnsAsync(emptyCommandResult); + ) + .Callback( + (opts, _, _) => + { + capturedOptions.Add(opts); + } + ) + .ReturnsAsync(TestHelpers.EmptyCommandResult()); + + var builder = TestHelpers.CreateSilentPipelineBuilder(new LoomContext(settings, tempDir), + services => + { + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + services.AddModule(); + }); + var pipeline = await builder.BuildAsync(); + await pipeline.RunAsync(); + + await Assert.That(capturedOptions[0].Configuration).IsEqualTo("Debug"); + } - var builder = CreateSilentPipelineBuilder(settings, tempDir, mockDotNet); - var pipeline = await builder.BuildAsync(); - await pipeline.RunAsync(); + [Test] + public async Task ExecuteAsync_PassesVersionProperties_FromMinVer() + { + using var tempDir = new TempDirectory(); + var settings = CreateSettings(); + var mockDotNet = new Mock(); + DotNetBuildOptions? capturedOptions = null; - await Assert.That(capturedOptions[0].Configuration).IsEqualTo("Debug"); - } - finally - { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + + + mockDotNet + .Setup(d => + d.Build( + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .Callback((opts, _, _) => + { + capturedOptions = opts; + }) + .ReturnsAsync(TestHelpers.EmptyCommandResult()); + + var builder = TestHelpers.CreateSilentPipelineBuilder(new LoomContext(settings, tempDir), + services => + { + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + services.AddModule(); + }); + var pipeline = await builder.BuildAsync(); + await pipeline.RunAsync(); + + await Assert.That(capturedOptions).IsNotNull(); + + var properties = capturedOptions!.Properties!.ToDictionary(x => x.Key, x => x.Value); + await Assert.That(properties["AssemblyVersion"]).IsEqualTo("1.0.0.0"); + await Assert.That(properties["FileVersion"]).IsEqualTo("1.2.3.0"); + await Assert.That(properties["InformationalVersion"]).IsEqualTo("1.2.3"); + await Assert.That(properties["PackageVersion"]).IsEqualTo("1.2.3"); + await Assert.That(properties["Version"]).IsEqualTo("1.2.3"); } } diff --git a/src/Loom.Build.Tests/Unit/CleanModuleTests.cs b/src/Loom.Build.Tests/Unit/CleanModuleTests.cs index d1f001c..6eeb021 100644 --- a/src/Loom.Build.Tests/Unit/CleanModuleTests.cs +++ b/src/Loom.Build.Tests/Unit/CleanModuleTests.cs @@ -1,25 +1,19 @@ using Loom.Config; using Loom.Modules; -using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; -using ModularPipelines; + using ModularPipelines.DotNet.Options; using ModularPipelines.DotNet.Services; using ModularPipelines.Models; using ModularPipelines.Options; + using Moq; namespace Loom.Build.Tests.Unit; public class CleanModuleTests { - private static string CreateTemporaryDirectory() - { - var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - Directory.CreateDirectory(tempDirectory); - return tempDirectory; - } - private static LoomSettings CreateSettings(string?[]? additionalCleanDirectories = null) { return new LoomSettings @@ -37,195 +31,159 @@ private static LoomSettings CreateSettings(string?[]? additionalCleanDirectories }; } - private static PipelineBuilder CreateSilentPipelineBuilder( - LoomSettings settings, - string tempDir, - Mock mockDotNet - ) - { - var builder = Pipeline.CreateBuilder(); - builder.Services.AddSingleton(new LoomContext(settings, tempDir)); - builder.Services.AddSingleton(new ConfigurationBuilder().Build()); - builder.Services.AddSingleton(mockDotNet.Object); - builder.Services.AddModule(); - - builder.Options.PrintLogo = false; - builder.Options.ShowProgressInConsole = false; - builder.Options.PrintResults = false; - builder.Options.PrintDependencyChains = false; - builder.Options.DefaultLoggingOptions = CommandLoggingOptions.Silent; - - return builder; - } - [Test] public async Task ExecuteAsync_WhenDirectoryExists_DeletesDirectoryAndComputesBytesDeleted() { - var tempDir = CreateTemporaryDirectory(); - try - { - var artifactsPath = Path.Combine(tempDir, ".artifacts"); - Directory.CreateDirectory(artifactsPath); - var dummyFile = Path.Combine(artifactsPath, "dummy.txt"); - await File.WriteAllTextAsync(dummyFile, "12345"); // 5 bytes + using var tempDir = new TempDirectory(); + var artifactsPath = Path.Combine(tempDir, ".artifacts"); + Directory.CreateDirectory(artifactsPath); + var dummyFile = Path.Combine(artifactsPath, "dummy.txt"); + await File.WriteAllTextAsync(dummyFile, "12345"); // 5 bytes - var settings = CreateSettings(); - var mockDotNet = new Mock(); + var settings = CreateSettings(); + var mockDotNet = new Mock(); - var builder = CreateSilentPipelineBuilder(settings, tempDir, mockDotNet); + var builder = TestHelpers.CreateSilentPipelineBuilder(new LoomContext(settings, tempDir), + services => + { + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + }); - var pipeline = await builder.BuildAsync(); - var summary = await pipeline.RunAsync(); - var result = await summary.GetModule(); + var pipeline = await builder.BuildAsync(); + var summary = await pipeline.RunAsync(); + var result = await summary.GetModule(); - var cleanResult = result.ValueOrDefault; + var cleanResult = result.ValueOrDefault; - await Assert.That(cleanResult).IsNotNull(); - await Assert.That(cleanResult!.Success).IsTrue(); - await Assert.That(cleanResult.DirectoryExisted).IsTrue(); - await Assert.That(cleanResult.BytesDeleted).IsEqualTo(5L); - await Assert.That(Directory.Exists(artifactsPath)).IsFalse(); - } - finally - { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + await Assert.That(cleanResult).IsNotNull(); + await Assert.That(cleanResult!.Success).IsTrue(); + await Assert.That(cleanResult.DirectoryExisted).IsTrue(); + await Assert.That(cleanResult.BytesDeleted).IsEqualTo(5L); + await Assert.That(Directory.Exists(artifactsPath)).IsFalse(); } [Test] public async Task ExecuteAsync_WhenDirectoryDoesNotExist_ReturnsSuccessAndExistedFalse() { - var tempDir = CreateTemporaryDirectory(); - try - { - var settings = CreateSettings(); - var mockDotNet = new Mock(); + using var tempDir = new TempDirectory(); + var settings = CreateSettings(); + var mockDotNet = new Mock(); - var builder = CreateSilentPipelineBuilder(settings, tempDir, mockDotNet); + var builder = TestHelpers.CreateSilentPipelineBuilder(new LoomContext(settings, tempDir), + services => + { + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + }); - var pipeline = await builder.BuildAsync(); - var summary = await pipeline.RunAsync(); - var result = await summary.GetModule(); + var pipeline = await builder.BuildAsync(); + var summary = await pipeline.RunAsync(); + var result = await summary.GetModule(); - var cleanResult = result.ValueOrDefault; + var cleanResult = result.ValueOrDefault; - await Assert.That(cleanResult).IsNotNull(); - await Assert.That(cleanResult!.Success).IsTrue(); - await Assert.That(cleanResult.DirectoryExisted).IsFalse(); - await Assert.That(cleanResult.BytesDeleted).IsNull(); - } - finally - { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + await Assert.That(cleanResult).IsNotNull(); + await Assert.That(cleanResult!.Success).IsTrue(); + await Assert.That(cleanResult.DirectoryExisted).IsFalse(); + await Assert.That(cleanResult.BytesDeleted).IsNull(); } [Test] public async Task ExecuteAsync_ExecutesDotNetClean_AgainstWorkspaceSolution() { - var tempDir = CreateTemporaryDirectory(); - try - { - var settings = CreateSettings(); - var mockDotNet = new Mock(); - DotNetCleanOptions? capturedOptions = null; - - mockDotNet - .Setup(x => - x.Clean( - It.IsAny(), - It.IsAny(), - It.IsAny() - ) + using var tempDir = new TempDirectory(); + var settings = CreateSettings(); + var mockDotNet = new Mock(); + DotNetCleanOptions? capturedOptions = null; + + mockDotNet + .Setup(x => + x.Clean( + It.IsAny(), + It.IsAny(), + It.IsAny() ) - .Callback( - (options, _, _) => capturedOptions = options - ) - .ReturnsAsync((CommandResult)null!); - - var builder = CreateSilentPipelineBuilder(settings, tempDir, mockDotNet); + ) + .Callback( + (options, _, _) => capturedOptions = options + ) + .ReturnsAsync((CommandResult)null!); + + var builder = TestHelpers.CreateSilentPipelineBuilder(new LoomContext(settings, tempDir), + services => + { + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + }); - var pipeline = await builder.BuildAsync(); - var summary = await pipeline.RunAsync(); - var result = await summary.GetModule(); + var pipeline = await builder.BuildAsync(); + var summary = await pipeline.RunAsync(); + var result = await summary.GetModule(); - await Assert.That(result.IsSuccess).IsTrue(); - await Assert.That(capturedOptions).IsNotNull(); - await Assert.That(capturedOptions!.ProjectSolution).IsEqualTo("test.sln"); - } - finally - { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + await Assert.That(result.IsSuccess).IsTrue(); + await Assert.That(capturedOptions).IsNotNull(); + await Assert.That(capturedOptions!.ProjectSolution).IsEqualTo("test.sln"); } [Test] public async Task ExecuteAsync_WithAdditionalCleanDirectories_DeletesAllConfiguredDirectories() { - var tempDir = CreateTemporaryDirectory(); - try - { - var artifactsPath = Path.Combine(tempDir, ".artifacts"); - Directory.CreateDirectory(artifactsPath); + using var tempDir = new TempDirectory(); + var artifactsPath = Path.Combine(tempDir, ".artifacts"); + Directory.CreateDirectory(artifactsPath); - var nodeModulesPath = Path.Combine(tempDir, "node_modules"); - Directory.CreateDirectory(nodeModulesPath); + var nodeModulesPath = Path.Combine(tempDir, "node_modules"); + Directory.CreateDirectory(nodeModulesPath); - var testResultsPath = Path.Combine(tempDir, "TestResults"); - Directory.CreateDirectory(testResultsPath); + var testResultsPath = Path.Combine(tempDir, "TestResults"); + Directory.CreateDirectory(testResultsPath); - var settings = CreateSettings(["node_modules", "TestResults"]); - var mockDotNet = new Mock(); + var settings = CreateSettings(["node_modules", "TestResults"]); + var mockDotNet = new Mock(); - var builder = CreateSilentPipelineBuilder(settings, tempDir, mockDotNet); - - var pipeline = await builder.BuildAsync(); - var summary = await pipeline.RunAsync(); - var result = await summary.GetModule(); - - await Assert.That(result.IsSuccess).IsTrue(); - await Assert.That(Directory.Exists(artifactsPath)).IsFalse(); - await Assert.That(Directory.Exists(nodeModulesPath)).IsFalse(); - await Assert.That(Directory.Exists(testResultsPath)).IsFalse(); - } - finally - { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + var builder = TestHelpers.CreateSilentPipelineBuilder(new LoomContext(settings, tempDir), + services => + { + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + }); + + var pipeline = await builder.BuildAsync(); + var summary = await pipeline.RunAsync(); + var result = await summary.GetModule(); + + await Assert.That(result.IsSuccess).IsTrue(); + await Assert.That(Directory.Exists(artifactsPath)).IsFalse(); + await Assert.That(Directory.Exists(nodeModulesPath)).IsFalse(); + await Assert.That(Directory.Exists(testResultsPath)).IsFalse(); } [Test] public async Task ExecuteAsync_WithOverlappingCleanDirectories_HandlesGracefullyWithoutThrowing() { - var tempDir = CreateTemporaryDirectory(); - try - { - var artifactsPath = Path.Combine(tempDir, ".artifacts"); - Directory.CreateDirectory(artifactsPath); + using var tempDir = new TempDirectory(); + var artifactsPath = Path.Combine(tempDir, ".artifacts"); + Directory.CreateDirectory(artifactsPath); - var innerPath = Path.Combine(artifactsPath, "nested"); - Directory.CreateDirectory(innerPath); + var innerPath = Path.Combine(artifactsPath, "nested"); + Directory.CreateDirectory(innerPath); - var settings = CreateSettings([".artifacts/nested", ".artifacts"]); // Exact overlap and child path - var mockDotNet = new Mock(); + var settings = CreateSettings([".artifacts/nested", ".artifacts"]); // Exact overlap and child path + var mockDotNet = new Mock(); - var builder = CreateSilentPipelineBuilder(settings, tempDir, mockDotNet); + var builder = TestHelpers.CreateSilentPipelineBuilder(new LoomContext(settings, tempDir), + services => + { + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + }); - var pipeline = await builder.BuildAsync(); - var summary = await pipeline.RunAsync(); - var result = await summary.GetModule(); + var pipeline = await builder.BuildAsync(); + var summary = await pipeline.RunAsync(); + var result = await summary.GetModule(); - await Assert.That(result.IsSuccess).IsTrue(); - await Assert.That(Directory.Exists(artifactsPath)).IsFalse(); - } - finally - { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + await Assert.That(result.IsSuccess).IsTrue(); + await Assert.That(Directory.Exists(artifactsPath)).IsFalse(); } } diff --git a/src/Loom.Build.Tests/Unit/NugetUploadModuleTests.cs b/src/Loom.Build.Tests/Unit/NugetUploadModuleTests.cs index 6d72ba6..2f068c6 100644 --- a/src/Loom.Build.Tests/Unit/NugetUploadModuleTests.cs +++ b/src/Loom.Build.Tests/Unit/NugetUploadModuleTests.cs @@ -1,30 +1,22 @@ using Loom.Config; using Loom.Modules; -using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using ModularPipelines; + using ModularPipelines.Context; using ModularPipelines.DotNet.Options; using ModularPipelines.DotNet.Services; using ModularPipelines.FileSystem; using ModularPipelines.Models; -using ModularPipelines.Modules; using ModularPipelines.Options; + using Moq; -using File = ModularPipelines.FileSystem.File; namespace Loom.Build.Tests.Unit; public class NugetUploadModuleTests { - private static string CreateTemporaryDirectory() - { - var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - Directory.CreateDirectory(tempDirectory); - return tempDirectory; - } - private static LoomSettings CreateSettings( bool withNugetArtifact = true, bool enableNugetUpload = true @@ -53,146 +45,99 @@ private static LoomSettings CreateSettings( return settings; } - private static PipelineBuilder CreateSilentPipelineBuilder( - LoomSettings settings, - string tempDir, - Mock mockDotNet - ) - { - var builder = Pipeline.CreateBuilder(); - builder.Services.AddSingleton(new LoomContext(settings, tempDir)); - - builder.Services.AddSingleton(new ConfigurationBuilder().Build()); - builder.Services.AddSingleton(mockDotNet.Object); - builder.Services.AddModule(); - - // We'll mock the PackModule so NugetUploadModule has packages to upload - var mockPackModule = new Mock(new LoomContext(settings, tempDir)); - builder.Services.AddSingleton(mockPackModule.Object); - - builder.Options.PrintLogo = false; - builder.Options.ShowProgressInConsole = false; - builder.Options.PrintResults = false; - builder.Options.PrintDependencyChains = false; - builder.Options.DefaultLoggingOptions = CommandLoggingOptions.Silent; - - return builder; - } - [Test] public async Task Configure_SkipsExecution_WhenNoNugetArtifactsDefined() { - var tempDir = CreateTemporaryDirectory(); - try - { - var settings = CreateSettings(withNugetArtifact: false); - var mockDotNet = new Mock(); - var builder = CreateSilentPipelineBuilder(settings, tempDir, mockDotNet); - - var pipeline = await builder.BuildAsync(); - var summary = await pipeline.RunAsync(); - var result = await summary.GetModule(); - - await Assert.That(result.SkipDecisionOrDefault).IsNotNull(); - await Assert.That(result.SkipDecisionOrDefault!.ShouldSkip).IsTrue(); - await Assert.That(result.SkipDecisionOrDefault.Reason).Contains("No nuget artifacts"); - } - finally - { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + using var tempDir = new TempDirectory(); + var settings = CreateSettings(withNugetArtifact: false); + var mockDotNet = new Mock(); + var builder = TestHelpers.CreateSilentPipelineBuilder(new LoomContext(settings, tempDir), + services => + { + services.AddSingleton(mockDotNet.Object); + services.AddModule(x => new FakePackModule(x.GetRequiredService()) as PackModule); + services.AddModule(); + }); + + var pipeline = await builder.BuildAsync(); + var summary = await pipeline.RunAsync(); + var result = await summary.GetModule(); + + await Assert.That(result.SkipDecisionOrDefault).IsNotNull(); + await Assert.That(result.SkipDecisionOrDefault!.ShouldSkip).IsTrue(); + await Assert.That(result.SkipDecisionOrDefault.Reason).Contains("No nuget artifacts"); } [Test] public async Task Configure_SkipsExecution_WhenNugetUploadDisabled() { - var tempDir = CreateTemporaryDirectory(); - try - { - Directory.CreateDirectory(Path.Combine(tempDir, ".artifacts", "nuget")); - var settings = CreateSettings(withNugetArtifact: true, enableNugetUpload: false); - var mockDotNet = new Mock(); - var builder = CreateSilentPipelineBuilder(settings, tempDir, mockDotNet); - - var pipeline = await builder.BuildAsync(); - var summary = await pipeline.RunAsync(); - var result = await summary.GetModule(); - - await Assert.That(result.SkipDecisionOrDefault).IsNotNull(); - await Assert.That(result.SkipDecisionOrDefault!.ShouldSkip).IsTrue(); - await Assert - .That(result.SkipDecisionOrDefault.Reason) - .Contains("disabled in workspace settings"); - } - finally - { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + using var tempDir = new TempDirectory(); + Directory.CreateDirectory(Path.Combine(tempDir, ".artifacts", "nuget")); + var settings = CreateSettings(withNugetArtifact: true, enableNugetUpload: false); + var mockDotNet = new Mock(); + var builder = TestHelpers.CreateSilentPipelineBuilder(new LoomContext(settings, tempDir), + services => + { + services.AddSingleton(mockDotNet.Object); + services.AddModule(x => new FakePackModule(x.GetRequiredService()) as PackModule); + services.AddModule(); + }); + + var pipeline = await builder.BuildAsync(); + var summary = await pipeline.RunAsync(); + var result = await summary.GetModule(); + + await Assert.That(result.SkipDecisionOrDefault).IsNotNull(); + await Assert.That(result.SkipDecisionOrDefault!.ShouldSkip).IsTrue(); + await Assert + .That(result.SkipDecisionOrDefault.Reason) + .Contains("disabled in workspace settings"); } [Test] public async Task ExecuteAsync_PushesPackages_WhenConditionsAreMet() { - var tempDir = CreateTemporaryDirectory(); - try - { - Directory.CreateDirectory(Path.Combine(tempDir, ".artifacts", "nuget")); - var settings = CreateSettings(withNugetArtifact: true, enableNugetUpload: true); - - var capturedOptions = new List(); - - var mockDotNet = new Mock(); - - var emptyCommandResult = new CommandResult( - "", - "", - "", - "", - new Dictionary(), - DateTimeOffset.UtcNow, - DateTimeOffset.UtcNow, - TimeSpan.Zero, - 0 - ); + using var tempDir = new TempDirectory(); + Directory.CreateDirectory(Path.Combine(tempDir, ".artifacts", "nuget")); + var settings = CreateSettings(withNugetArtifact: true, enableNugetUpload: true); - var mockCommand = new Mock(); - var mockNuget = new Mock(mockCommand.Object); + var capturedOptions = new List(); - mockNuget - .Setup(n => - n.Push( - It.IsAny(), - It.IsAny(), - It.IsAny() - ) - ) - .Callback( - (opts, _, _) => capturedOptions.Add(opts) - ) - .ReturnsAsync(emptyCommandResult); + var mockDotNet = new Mock(); - mockDotNet.Setup(d => d.Nuget).Returns(mockNuget.Object); - var builder = Pipeline.CreateBuilder(); - var loomContext = new LoomContext(settings, tempDir); - // Simulate CI mode to avoid ctx.IsRunningLocally() skipping it - Environment.SetEnvironmentVariable("LOOM_IGNORE_LOCAL_CHECK", "true"); + var mockCommand = new Mock(); + var mockNuget = new Mock(mockCommand.Object); - builder.Services.AddSingleton(loomContext); - builder.Services.AddSingleton(new ConfigurationBuilder().Build()); - builder.Services.AddSingleton(mockDotNet.Object); + mockNuget + .Setup(n => + n.Push( + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .Callback( + (opts, _, _) => capturedOptions.Add(opts) + ) + .ReturnsAsync(TestHelpers.EmptyCommandResult()); - builder.Services.AddModule(x => - new FakePackModule(x.GetRequiredService()) as PackModule - ); - builder.Services.AddModule(); - builder.Options.PrintLogo = false; - builder.Options.ShowProgressInConsole = false; - builder.Options.PrintResults = false; - builder.Options.PrintDependencyChains = false; + mockDotNet.Setup(d => d.Nuget).Returns(mockNuget.Object); + + var loomContext = new LoomContext(settings, tempDir); + + // Simulate CI mode to avoid ctx.IsRunningLocally() skipping it + Environment.SetEnvironmentVariable("LOOM_IGNORE_LOCAL_CHECK", "true"); + try + { + var builder = TestHelpers.CreateSilentPipelineBuilder(loomContext, + services => + { + services.AddSingleton(mockDotNet.Object); + services.AddModule(x => new FakePackModule(x.GetRequiredService()) as PackModule); + services.AddModule(); + }); var pipeline = await builder.BuildAsync(); await pipeline.RunAsync(); @@ -204,25 +149,6 @@ public async Task ExecuteAsync_PushesPackages_WhenConditionsAreMet() finally { Environment.SetEnvironmentVariable("LOOM_IGNORE_LOCAL_CHECK", null); - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); } } } - -// Minimal mock to supply dependencies -public class FakePackModule : PackModule -{ - public FakePackModule(LoomContext buildContext) - : base(buildContext) { } - - protected override async Task ExecuteAsync( - IModuleContext context, - CancellationToken ct - ) - { - return new PackResult( - new List { new File("package1.nupkg"), new File("package2.nupkg") } - ); - } -} diff --git a/src/Loom.Build.Tests/Unit/PackModuleTests.cs b/src/Loom.Build.Tests/Unit/PackModuleTests.cs index 59d6704..af6e0bc 100644 --- a/src/Loom.Build.Tests/Unit/PackModuleTests.cs +++ b/src/Loom.Build.Tests/Unit/PackModuleTests.cs @@ -1,14 +1,13 @@ -using Loom.MinVer; using Loom.Modules; -using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; -using ModularPipelines; -using ModularPipelines.Context; + using ModularPipelines.DotNet.Options; using ModularPipelines.DotNet.Services; using ModularPipelines.FileSystem; using ModularPipelines.Models; using ModularPipelines.Options; + using Moq; namespace Loom.Build.Tests.Unit; @@ -21,255 +20,169 @@ public class PackModuleTests WorkingDirectory = "/test/working/directory", }; - private static string CreateTemporaryDirectory() - { - var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - Directory.CreateDirectory(tempDirectory); - return tempDirectory; - } - - private static PipelineBuilder CreateSilentPipelineBuilder( - LoomContext context, - string tempDir, - Mock mockDotNet, - Mock? mockProvider = null - ) - { - var builder = Pipeline.CreateBuilder(); - builder.Services.AddSingleton(context); - builder.Services.AddSingleton(new ConfigurationBuilder().Build()); - builder.Services.AddSingleton(mockDotNet.Object); - if (mockProvider?.Object is not null) - { - builder.Services.AddSingleton(mockProvider.Object); - } - builder.Services.AddModule(); - builder.Services.AddModule(); - builder.Services.AddModule(); - - builder.Options.PrintLogo = false; - builder.Options.ShowProgressInConsole = false; - builder.Options.PrintResults = false; - builder.Options.PrintDependencyChains = false; - builder.Options.DefaultLoggingOptions = CommandLoggingOptions.Silent; - - return builder; - } - [Test] public async Task Configure_SkipsExecution_WhenNoNugetArtifactsDefined() { - var tempDir = CreateTemporaryDirectory(); - try + using var tempDir = new TempDirectory(); + var mockDotNet = new Mock(); + var builder = TestHelpers.CreateSilentPipelineBuilder(_loomContext with { - var mockDotNet = new Mock(); - var builder = CreateSilentPipelineBuilder(_loomContext, tempDir, mockDotNet); - - var pipeline = await builder.BuildAsync(); - var summary = await pipeline.RunAsync(); - var result = await summary.GetModule(); - - await Assert.That(result.SkipDecisionOrDefault).IsNotNull(); - await Assert.That(result.SkipDecisionOrDefault!.ShouldSkip).IsTrue(); - } - finally + WorkingDirectory = tempDir, + }, services => { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + services.AddModule(); + services.AddModule(); + }); + + var pipeline = await builder.BuildAsync(); + var summary = await pipeline.RunAsync(); + var result = await summary.GetModule(); + + await Assert.That(result.SkipDecisionOrDefault).IsNotNull(); + await Assert.That(result.SkipDecisionOrDefault!.ShouldSkip).IsTrue(); } [Test] public async Task ExecuteAsync_IteratesAndPacksAllNugetArtifacts_WithCorrectVersionFromMinVer() { - var tempDir = CreateTemporaryDirectory(); - try + using var tempDir = new TempDirectory(); + var mockDotNet = new Mock(); + var context = _loomContext with { - var mockDotNet = new Mock(); - var context = _loomContext with + WorkingDirectory = tempDir, + Artifacts = new Dictionary { - WorkingDirectory = tempDir, - Artifacts = new Dictionary - { - ["package1"] = new() { Type = ArtifactType.Nuget, Project = "package1.csproj" }, - ["package2"] = new() { Type = ArtifactType.Nuget, Project = "package2.csproj" }, - }, - }; - - var capturedOptions = new List(); - - mockDotNet - .Setup(x => - x.Pack( - It.IsAny(), - It.IsAny(), - It.IsAny() - ) + ["package1"] = new() { Type = ArtifactType.Nuget, Project = "package1.csproj" }, + ["package2"] = new() { Type = ArtifactType.Nuget, Project = "package2.csproj" }, + }, + }; + + var capturedOptions = new List(); + + mockDotNet + .Setup(x => + x.Pack( + It.IsAny(), + It.IsAny(), + It.IsAny() ) - .Callback( - (options, _, _) => + ) + .Callback( + (options, _, _) => + { + capturedOptions.Add(options); + if (options.Output != null) { - capturedOptions.Add(options); - if (options.Output != null) - { - Directory.CreateDirectory(options.Output); - } + Directory.CreateDirectory(options.Output); } - ) - .ReturnsAsync( - new CommandResult( - "", - "", - "", - "", - new Dictionary(), - DateTimeOffset.UtcNow, - DateTimeOffset.UtcNow, - TimeSpan.Zero, - 0 - ) - ); - - List expectedFiles = - [ - Path.Combine(tempDir, ".artifacts", "nuget", "package1.1.2.3.nupkg"), - Path.Combine(tempDir, ".artifacts", "nuget", "package2.1.2.3.nupkg"), - ]; - - var mockProvider = new Mock(); - mockProvider - .Setup(p => - p.EnumerateFiles(It.IsAny(), "*", SearchOption.TopDirectoryOnly) - ) - .Returns(expectedFiles); - - var builder = CreateSilentPipelineBuilder(context, tempDir, mockDotNet, mockProvider); + } + ) + .ReturnsAsync(TestHelpers.EmptyCommandResult()); + + List expectedFiles = + [ + Path.Combine(tempDir, ".artifacts", "nuget", "package1.1.2.3.nupkg"), + Path.Combine(tempDir, ".artifacts", "nuget", "package2.1.2.3.nupkg"), + ]; + + var mockProvider = new Mock(); + mockProvider + .Setup(p => + p.EnumerateFiles(It.IsAny(), "*", SearchOption.TopDirectoryOnly) + ) + .Returns(expectedFiles); - var pipeline = await builder.BuildAsync(); - var summary = await pipeline.RunAsync(); + var builder = TestHelpers.CreateSilentPipelineBuilder(context, services => + { + services.AddSingleton(mockDotNet.Object); + services.AddSingleton(mockProvider.Object); + services.AddModule(); + services.AddModule(); + services.AddModule(); + }); - var moduleResult = await summary.GetModule(); + var pipeline = await builder.BuildAsync(); + var summary = await pipeline.RunAsync(); - await Assert.That(moduleResult.IsSuccess).IsTrue(); - await Assert.That(capturedOptions.Count).IsEqualTo(2); + var moduleResult = await summary.GetModule(); - foreach (var options in capturedOptions) - { - var version = options.Properties!.First(p => p.Key == "Version").Value; - await Assert.That(version).IsEqualTo("1.2.3"); - } + await Assert.That(moduleResult.IsSuccess).IsTrue(); + await Assert.That(capturedOptions.Count).IsEqualTo(2); - var packResult = moduleResult.ValueOrDefault!; - await Assert - .That(packResult.Artifacts.Select(a => a.OriginalPath)) - .IsEquivalentTo(expectedFiles); - } - finally + foreach (var options in capturedOptions) { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); + var properties = options.Properties!.ToDictionary(p => p.Key, p => p.Value); + await Assert.That(properties["AssemblyVersion"]).IsEqualTo("1.0.0.0"); + await Assert.That(properties["FileVersion"]).IsEqualTo("1.2.3.0"); + await Assert.That(properties["InformationalVersion"]).IsEqualTo("1.2.3"); + await Assert.That(properties["PackageVersion"]).IsEqualTo("1.2.3"); + await Assert.That(properties["Version"]).IsEqualTo("1.2.3"); } + + var packResult = moduleResult.ValueOrDefault!; + await Assert + .That(packResult.Artifacts.Select(a => a.OriginalPath)) + .IsEquivalentTo(expectedFiles); } [Test] public async Task ExecuteAsync_UsesPrefixVersion_WhenMatches() { - var tempDir = CreateTemporaryDirectory(); - try + using var tempDir = new TempDirectory(); + var mockDotNet = new Mock(); + var context = _loomContext with { - var mockDotNet = new Mock(); - var context = _loomContext with + WorkingDirectory = tempDir, + Artifacts = new Dictionary { - WorkingDirectory = tempDir, - Artifacts = new Dictionary + ["prefixed"] = new() { - ["prefixed"] = new() - { - Type = ArtifactType.Nuget, - Project = "prefixed.csproj", - TagPrefix = "v", - }, + Type = ArtifactType.Nuget, + Project = "prefixed.csproj", + TagPrefix = "v", }, - }; - - DotNetPackOptions? captured = null; - mockDotNet - .Setup(x => - x.Pack( - It.IsAny(), - It.IsAny(), - It.IsAny() - ) + }, + }; + + DotNetPackOptions? captured = null; + mockDotNet + .Setup(x => + x.Pack( + It.IsAny(), + It.IsAny(), + It.IsAny() ) - .Callback( - (o, _, _) => - { - captured = o; - if (o.Output != null) - Directory.CreateDirectory(o.Output); - } - ) - .ReturnsAsync( - new CommandResult( - "", - "", - "", - "", - new Dictionary(), - DateTimeOffset.UtcNow, - DateTimeOffset.UtcNow, - TimeSpan.Zero, - 0 - ) - ); - - var builder = CreateSilentPipelineBuilder(context, tempDir, mockDotNet); - var pipeline = await builder.BuildAsync(); - await pipeline.RunAsync(); - - await Assert.That(captured).IsNotNull(); - var version = captured!.Properties!.First(p => p.Key == "Version").Value; - // FakeMinVerModule returns "1.2.4" for prefix "v" - await Assert.That(version).IsEqualTo("1.2.4"); - } - finally - { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } - } -} - -public class FakeBuildModule : BuildModule -{ - public FakeBuildModule(LoomContext loomContext) - : base(loomContext) { } - - protected override Task ExecuteAsync(IModuleContext context, CancellationToken ct) - { - return Task.FromResult(new BuildResult("success")); - } -} - -public class FakeMinVerModule : MinVerModule -{ - public static readonly MinVerVersion MinVer123 = new("1.2.3"); - public static readonly MinVerVersion MinVer124 = new("1.2.4"); - - public FakeMinVerModule(LoomContext loomContext) - : base(loomContext) { } - - protected override Task ExecuteAsync( - IModuleContext context, - CancellationToken ct - ) => - Task.FromResult( - new MinVerResult( - new Dictionary + ) + .Callback( + (o, _, _) => { - [string.Empty] = MinVer123, - ["v"] = MinVer124, + captured = o; + if (o.Output != null) + Directory.CreateDirectory(o.Output); } ) - ); + .ReturnsAsync(TestHelpers.EmptyCommandResult()); + + var builder = TestHelpers.CreateSilentPipelineBuilder(context, services => + { + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + services.AddModule(); + services.AddModule(); + }); + var pipeline = await builder.BuildAsync(); + await pipeline.RunAsync(); + + await Assert.That(captured).IsNotNull(); + var properties = captured!.Properties!.ToDictionary(p => p.Key, p => p.Value); + await Assert.That(properties["AssemblyVersion"]).IsEqualTo("1.0.0.0"); + await Assert.That(properties["FileVersion"]).IsEqualTo("1.2.4.0"); + await Assert.That(properties["InformationalVersion"]).IsEqualTo("1.2.4"); + await Assert.That(properties["PackageVersion"]).IsEqualTo("1.2.4"); + var version = properties["Version"]; + // FakeMinVerModule returns "1.2.4" for prefix "v" + await Assert.That(version).IsEqualTo("1.2.4"); + } } diff --git a/src/Loom.Build.Tests/Unit/PublishModuleTests.cs b/src/Loom.Build.Tests/Unit/PublishModuleTests.cs index bbe9b36..0c18206 100644 --- a/src/Loom.Build.Tests/Unit/PublishModuleTests.cs +++ b/src/Loom.Build.Tests/Unit/PublishModuleTests.cs @@ -1,10 +1,8 @@ using Loom.Config; using Loom.Modules; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using ModularPipelines; using ModularPipelines.DotNet.Options; using ModularPipelines.DotNet.Services; using ModularPipelines.Models; @@ -16,13 +14,6 @@ namespace Loom.Build.Tests.Unit; public class PublishModuleTests { - private static string CreateTemporaryDirectory() - { - var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - Directory.CreateDirectory(tempDirectory); - return tempDirectory; - } - private static LoomContext CreateTestContext(bool withPublishableArtifact = true, string tempDir = "/test") { var settings = new LoomSettings @@ -46,7 +37,12 @@ private static LoomContext CreateTestContext(bool withPublishableArtifact = true resolved.Add(new ResolvedArtifact("MyApp", myApp, "win-x64", IsAot: false, CanBuildOnHost: true)); // Artifact 2: No RID, falls back to Global "linux-x64" - var myVelo = new ArtifactSettings { Type = ArtifactType.Velopack, Project = "MyVelopack.csproj" }; + var myVelo = new ArtifactSettings + { + Type = ArtifactType.Velopack, + Project = "MyVelopack.csproj", + TagPrefix = "v", + }; settings.Artifacts.Add("MyVelopack", myVelo); resolved.Add(new ResolvedArtifact("MyVelopack", myVelo, "linux-x64", IsAot: false, CanBuildOnHost: true)); } @@ -64,177 +60,163 @@ private static LoomContext CreateTestContext(bool withPublishableArtifact = true }; } - private static PipelineBuilder CreateSilentPipelineBuilder( - LoomContext context, - Mock mockDotNet - ) - { - var builder = Pipeline.CreateBuilder(); - builder.Services.AddSingleton(context); - builder.Services.AddSingleton(new ConfigurationBuilder().Build()); - builder.Services.AddSingleton(mockDotNet.Object); - builder.Services.AddModule(); - - builder.Options.PrintLogo = false; - builder.Options.ShowProgressInConsole = false; - builder.Options.PrintResults = false; - builder.Options.PrintDependencyChains = false; - builder.Options.DefaultLoggingOptions = CommandLoggingOptions.Silent; - - return builder; - } - [Test] public async Task Configure_SkipsExecution_WhenNoPublishableArtifactsDefined() { - var tempDir = CreateTemporaryDirectory(); - try + using var tempDir = new TempDirectory(); + var context = CreateTestContext(false, tempDir); + var mockDotNet = new Mock(); + var builder = TestHelpers.CreateSilentPipelineBuilder(context, services => { - var context = CreateTestContext(false, CreateTemporaryDirectory()); - var mockDotNet = new Mock(); - var builder = CreateSilentPipelineBuilder(context, mockDotNet); + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + services.AddModule(); + }); - var pipeline = await builder.BuildAsync(); - var summary = await pipeline.RunAsync(); - var result = await summary.GetModule(); + var pipeline = await builder.BuildAsync(); + var summary = await pipeline.RunAsync(); + var result = await summary.GetModule(); - await Assert.That(result.SkipDecisionOrDefault).IsNotNull(); - await Assert.That(result.SkipDecisionOrDefault!.ShouldSkip).IsTrue(); - } - finally - { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + await Assert.That(result.SkipDecisionOrDefault).IsNotNull(); + await Assert.That(result.SkipDecisionOrDefault!.ShouldSkip).IsTrue(); } [Test] public async Task ExecuteAsync_DeletesExistingPublishDirectory_BeforePublishing() { - var tempDir = CreateTemporaryDirectory(); - try - { - var publishDir = Path.Combine(tempDir, ".artifacts", "publish", "MyApp", "win-x64"); - Directory.CreateDirectory(publishDir); - var dummyFile = Path.Combine(publishDir, "old-binary.dll"); - await File.WriteAllTextAsync(dummyFile, "dummy"); - - var context = CreateTestContext(true, tempDir); - var mockDotNet = new Mock(); - mockDotNet - .Setup(x => - x.Publish( - It.IsAny(), - It.IsAny(), - It.IsAny() - ) + using var tempDir = new TempDirectory(); + var publishDir = Path.Combine(tempDir, ".artifacts", "publish", "MyApp", "win-x64"); + Directory.CreateDirectory(publishDir); + var dummyFile = Path.Combine(publishDir, "old-binary.dll"); + await File.WriteAllTextAsync(dummyFile, "dummy"); + + var context = CreateTestContext(true, tempDir); + var mockDotNet = new Mock(); + mockDotNet + .Setup(x => + x.Publish( + It.IsAny(), + It.IsAny(), + It.IsAny() ) - .ReturnsAsync((CommandResult)null!); + ) + .ReturnsAsync((CommandResult)null!); - var builder = CreateSilentPipelineBuilder(context, mockDotNet); - var pipeline = await builder.BuildAsync(); - await pipeline.RunAsync(); - - await Assert.That(File.Exists(dummyFile)).IsFalse(); - // Publish will recreate the folder theoretically, but the initial delete should clear old files natively. - } - finally + var builder = TestHelpers.CreateSilentPipelineBuilder(context, services => { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + services.AddModule(); + }); + var pipeline = await builder.BuildAsync(); + await pipeline.RunAsync(); + + await Assert.That(File.Exists(dummyFile)).IsFalse(); + // Publish will recreate the folder theoretically, but the initial delete should clear old files natively. } [Test] public async Task ExecuteAsync_ResolvesRid_FromArtifactSettingsFirstThenContext() { - var tempDir = CreateTemporaryDirectory(); - try - { - var context = CreateTestContext(true, tempDir); - var mockDotNet = new Mock(); - var capturedOptions = new List(); - - mockDotNet - .Setup(x => - x.Publish( - It.IsAny(), - It.IsAny(), - It.IsAny() - ) - ) - .Callback( - (options, _, _) => capturedOptions.Add(options) + using var tempDir = new TempDirectory(); + var context = CreateTestContext(true, tempDir); + var mockDotNet = new Mock(); + var capturedOptions = new List(); + + mockDotNet + .Setup(x => + x.Publish( + It.IsAny(), + It.IsAny(), + It.IsAny() ) - .ReturnsAsync((CommandResult)null!); - - var builder = CreateSilentPipelineBuilder(context, mockDotNet); - var pipeline = await builder.BuildAsync(); - await pipeline.RunAsync(); - - await Assert.That(capturedOptions).Count().IsEqualTo(2); + ) + .Callback( + (options, _, _) => capturedOptions.Add(options) + ) + .ReturnsAsync((CommandResult)null!); - // MyApp specified win-x64 - await Assert - .That( - capturedOptions.Any(x => - x.Runtime == "win-x64" && x.ProjectSolution == "MyApp.csproj" - ) + var builder = TestHelpers.CreateSilentPipelineBuilder(context, services => + { + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + services.AddModule(); + }); + var pipeline = await builder.BuildAsync(); + await pipeline.RunAsync(); + + await Assert.That(capturedOptions).Count().IsEqualTo(2); + + // MyApp specified win-x64 + await Assert + .That( + capturedOptions.Any(x => + x.Runtime == "win-x64" && x.ProjectSolution == "MyApp.csproj" ) - .IsTrue(); - - // MyVelopack had no RID, falls back to context linux-x64 - await Assert - .That( - capturedOptions.Any(x => - x.Runtime == "linux-x64" && x.ProjectSolution == "MyVelopack.csproj" - ) + ) + .IsTrue(); + + // MyVelopack had no RID, falls back to context linux-x64 + await Assert + .That( + capturedOptions.Any(x => + x.Runtime == "linux-x64" && x.ProjectSolution == "MyVelopack.csproj" ) - .IsTrue(); - } - finally - { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + ) + .IsTrue(); + + var appOptions = capturedOptions.First(x => x.ProjectSolution == "MyApp.csproj"); + var veloOptions = capturedOptions.First(x => x.ProjectSolution == "MyVelopack.csproj"); + + var appProperties = appOptions.Properties!.ToDictionary(x => x.Key, x => x.Value); + var veloProperties = veloOptions.Properties!.ToDictionary(x => x.Key, x => x.Value); + + await Assert.That(appProperties["AssemblyVersion"]).IsEqualTo("1.0.0.0"); + await Assert.That(appProperties["FileVersion"]).IsEqualTo("1.2.3.0"); + await Assert.That(appProperties["InformationalVersion"]).IsEqualTo("1.2.3"); + await Assert.That(appProperties["PackageVersion"]).IsEqualTo("1.2.3"); + await Assert.That(appProperties["Version"]).IsEqualTo("1.2.3"); + + await Assert.That(veloProperties["AssemblyVersion"]).IsEqualTo("1.0.0.0"); + await Assert.That(veloProperties["FileVersion"]).IsEqualTo("1.2.4.0"); + await Assert.That(veloProperties["InformationalVersion"]).IsEqualTo("1.2.4"); + await Assert.That(veloProperties["PackageVersion"]).IsEqualTo("1.2.4"); + await Assert.That(veloProperties["Version"]).IsEqualTo("1.2.4"); } [Test] public async Task ExecuteAsync_ReturnsPublishResult_WrappingPublishedArtifacts() { - var tempDir = CreateTemporaryDirectory(); - try - { - var context = CreateTestContext(true, tempDir); - var mockDotNet = new Mock(); - - mockDotNet - .Setup(x => - x.Publish( - It.IsAny(), - It.IsAny(), - It.IsAny() - ) + using var tempDir = new TempDirectory(); + var context = CreateTestContext(true, tempDir); + var mockDotNet = new Mock(); + + mockDotNet + .Setup(x => + x.Publish( + It.IsAny(), + It.IsAny(), + It.IsAny() ) - .ReturnsAsync((CommandResult)null!); - - var builder = CreateSilentPipelineBuilder(context, mockDotNet); - var pipeline = await builder.BuildAsync(); - var summary = await pipeline.RunAsync(); - var moduleResult = await summary.GetModule(); + ) + .ReturnsAsync((CommandResult)null!); - var result = moduleResult.ValueOrDefault; - - await Assert.That(result).IsNotNull(); // Expecting PublishResult here - await Assert.That(result!.Artifacts).Count().IsEqualTo(2); - await Assert - .That(result.Artifacts.Any(x => x.ArtifactName == "MyApp" && x.Rid == "win-x64")) - .IsTrue(); - } - finally + var builder = TestHelpers.CreateSilentPipelineBuilder(context, services => { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + services.AddModule(); + }); + var pipeline = await builder.BuildAsync(); + var summary = await pipeline.RunAsync(); + var moduleResult = await summary.GetModule(); + + var result = moduleResult.ValueOrDefault; + + await Assert.That(result).IsNotNull(); // Expecting PublishResult here + await Assert.That(result!.Artifacts).Count().IsEqualTo(2); + await Assert + .That(result.Artifacts.Any(x => x.ArtifactName == "MyApp" && x.Rid == "win-x64")) + .IsTrue(); } } diff --git a/src/Loom.Build.Tests/Unit/RestoreModuleTests.cs b/src/Loom.Build.Tests/Unit/RestoreModuleTests.cs index db95e97..c80c3eb 100644 --- a/src/Loom.Build.Tests/Unit/RestoreModuleTests.cs +++ b/src/Loom.Build.Tests/Unit/RestoreModuleTests.cs @@ -1,14 +1,10 @@ using Loom.Config; using Loom.Modules; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using ModularPipelines; -using ModularPipelines.Context; using ModularPipelines.DotNet.Options; using ModularPipelines.DotNet.Services; -using ModularPipelines.Models; using ModularPipelines.Options; using Moq; @@ -17,13 +13,6 @@ namespace Loom.Build.Tests.Unit; public class RestoreModuleTests { - private static string CreateTemporaryDirectory() - { - var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - Directory.CreateDirectory(tempDirectory); - return tempDirectory; - } - private static LoomSettings CreateSettings() { return new LoomSettings @@ -42,130 +31,84 @@ private static LoomSettings CreateSettings() }; } - private static PipelineBuilder CreateSilentPipelineBuilder( - LoomSettings settings, - string tempDir, - Mock mockDotNet - ) - { - var builder = Pipeline.CreateBuilder(); - builder.Services.AddSingleton(new LoomContext(settings, tempDir)); - builder.Services.AddSingleton(new ConfigurationBuilder().Build()); - builder.Services.AddSingleton(mockDotNet.Object); - builder.Services.AddModule(); - - builder.Options.PrintLogo = false; - builder.Options.ShowProgressInConsole = false; - builder.Options.PrintResults = false; - builder.Options.PrintDependencyChains = false; - builder.Options.DefaultLoggingOptions = CommandLoggingOptions.Silent; - - return builder; - } - [Test] public async Task ExecuteAsync_PassesCorrectOptionsToDotNetRestore() { - var tempDir = CreateTemporaryDirectory(); - try - { - var settings = CreateSettings(); - var mockDotNet = new Mock(); - - var capturedOptions = new List(); - var capturedExecOptions = new List(); - - var emptyCommandResult = new CommandResult( - "", - "", - "", - "", - new Dictionary(), - DateTimeOffset.UtcNow, - DateTimeOffset.UtcNow, - TimeSpan.Zero, - 0 - ); - - mockDotNet - .Setup(d => - d.Restore( - It.IsAny(), - It.IsAny(), - It.IsAny() - ) - ) - .Callback( - (opts, execOpts, _) => - { - capturedOptions.Add(opts); - capturedExecOptions.Add(execOpts); - } - ) - .ReturnsAsync(emptyCommandResult); + using var tempDir = new TempDirectory(); + var settings = CreateSettings(); + var mockDotNet = new Mock(); - var builder = CreateSilentPipelineBuilder(settings, tempDir, mockDotNet); - var pipeline = await builder.BuildAsync(); - await pipeline.RunAsync(); + var capturedOptions = new List(); + var capturedExecOptions = new List(); - await Assert.That(capturedOptions).Count().IsEqualTo(1); - await Assert.That(capturedOptions[0].ProjectSolution).IsEqualTo("test.sln"); - await Assert.That(capturedExecOptions).Count().IsEqualTo(1); - await Assert.That(capturedExecOptions[0].WorkingDirectory).IsEqualTo(tempDir); - } - finally - { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + + mockDotNet + .Setup(d => + d.Restore( + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .Callback( + (opts, execOpts, _) => + { + capturedOptions.Add(opts); + capturedExecOptions.Add(execOpts); + } + ) + .ReturnsAsync(TestHelpers.EmptyCommandResult()); + + var builder = TestHelpers.CreateSilentPipelineBuilder(new LoomContext(settings, tempDir), + services => + { + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + }); + var pipeline = await builder.BuildAsync(); + await pipeline.RunAsync(); + + await Assert.That(capturedOptions).Count().IsEqualTo(1); + await Assert.That(capturedOptions[0].ProjectSolution).IsEqualTo("test.sln"); + + await Assert.That(capturedExecOptions).Count().IsEqualTo(1); + await Assert.That(capturedExecOptions[0].WorkingDirectory).IsEqualTo(tempDir.Path); } [Test] public async Task ExecuteAsync_ReturnsRestoreResult_WrappingCommandResult() { - var tempDir = CreateTemporaryDirectory(); - try - { - var settings = CreateSettings(); - var mockDotNet = new Mock(); - var emptyCommandResult = new CommandResult( - "", - "", - "", - "", - new Dictionary(), - DateTimeOffset.UtcNow, - DateTimeOffset.UtcNow, - TimeSpan.Zero, - 0 - ); - - mockDotNet - .Setup(d => - d.Restore( - It.IsAny(), - It.IsAny(), - It.IsAny() - ) + using var tempDir = new TempDirectory(); + var settings = CreateSettings(); + var mockDotNet = new Mock(); + var emptyCommandResult = TestHelpers.EmptyCommandResult(); + + + mockDotNet + .Setup(d => + d.Restore( + It.IsAny(), + It.IsAny(), + It.IsAny() ) - .ReturnsAsync(emptyCommandResult); - - var builder = CreateSilentPipelineBuilder(settings, tempDir, mockDotNet); - var pipeline = await builder.BuildAsync(); - var summary = await pipeline.RunAsync(); - var moduleResult = await summary.GetModule(); - - var val = moduleResult.ValueOrDefault; + ) + .ReturnsAsync(emptyCommandResult); - await Assert.That(val).IsNotNull(); // Expecting RestoreResult here - await Assert.That(val!.CommandResult).IsNotNull(); - await Assert.That(val!.CommandResult).IsEqualTo(emptyCommandResult); - } - finally - { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + var builder = TestHelpers.CreateSilentPipelineBuilder(new LoomContext(settings, tempDir), + services => + { + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + }); + var pipeline = await builder.BuildAsync(); + var summary = await pipeline.RunAsync(); + var moduleResult = await summary.GetModule(); + + var val = moduleResult.ValueOrDefault; + + await Assert.That(val).IsNotNull(); // Expecting RestoreResult here + await Assert.That(val!.CommandResult).IsNotNull(); + await Assert.That(val!.CommandResult).IsEqualTo(emptyCommandResult); } } diff --git a/src/Loom.Build.Tests/Unit/RestoreToolsModuleTests.cs b/src/Loom.Build.Tests/Unit/RestoreToolsModuleTests.cs index 232f5cd..d120c51 100644 --- a/src/Loom.Build.Tests/Unit/RestoreToolsModuleTests.cs +++ b/src/Loom.Build.Tests/Unit/RestoreToolsModuleTests.cs @@ -1,26 +1,20 @@ using Loom.Config; using Loom.Modules; -using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; -using ModularPipelines; + using ModularPipelines.Context; using ModularPipelines.DotNet.Options; using ModularPipelines.DotNet.Services; using ModularPipelines.Models; using ModularPipelines.Options; + using Moq; namespace Loom.Build.Tests.Unit; public class RestoreToolsModuleTests { - private static string CreateTemporaryDirectory() - { - var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - Directory.CreateDirectory(tempDirectory); - return tempDirectory; - } - private static LoomSettings CreateSettings( bool requiresMinVer = true, bool requiresVelopack = false @@ -59,27 +53,6 @@ private static LoomSettings CreateSettings( return settings; } - private static PipelineBuilder CreateSilentPipelineBuilder( - LoomSettings settings, - string tempDir, - Mock mockDotNet - ) - { - var builder = Pipeline.CreateBuilder(); - builder.Services.AddSingleton(new LoomContext(settings, tempDir)); - builder.Services.AddSingleton(new ConfigurationBuilder().Build()); - builder.Services.AddSingleton(mockDotNet.Object); - builder.Services.AddModule(); - - builder.Options.PrintLogo = false; - builder.Options.ShowProgressInConsole = false; - builder.Options.PrintResults = false; - builder.Options.PrintDependencyChains = false; - builder.Options.DefaultLoggingOptions = CommandLoggingOptions.Silent; - - return builder; - } - private static void SetupDotNetMocks( Mock mockDotNet, out List newOptions, @@ -91,18 +64,6 @@ out List toolOptions var outRestoreOptions = new List(); var outToolOptions = new List(); - var emptyCommandResult = new CommandResult( - "", - "", - "", - "", - new Dictionary(), - DateTimeOffset.UtcNow, - DateTimeOffset.UtcNow, - TimeSpan.Zero, - 0 - ); - var mockCommand = new Mock(); var mockNew = new Mock(mockCommand.Object); @@ -117,7 +78,7 @@ out List toolOptions .Callback( (opts, _, _) => outNewOptions.Add(opts) ) - .ReturnsAsync(emptyCommandResult); + .ReturnsAsync(TestHelpers.EmptyCommandResult()); var mockTool = new Mock(mockCommand.Object); mockTool @@ -131,7 +92,7 @@ out List toolOptions .Callback( (opts, _, _) => outRestoreOptions.Add(opts) ) - .ReturnsAsync(emptyCommandResult); + .ReturnsAsync(TestHelpers.EmptyCommandResult()); mockTool .Setup(t => @@ -144,7 +105,7 @@ out List toolOptions .Callback( (opts, _, _) => outToolOptions.Add(opts) ) - .ReturnsAsync(emptyCommandResult); + .ReturnsAsync(TestHelpers.EmptyCommandResult()); mockDotNet.Setup(d => d.New).Returns(mockNew.Object); mockDotNet.Setup(d => d.Tool).Returns(mockTool.Object); @@ -157,78 +118,69 @@ out List toolOptions [Test] public async Task Configure_SkipsExecution_WhenNoToolsAreRequired() { - var tempDir = CreateTemporaryDirectory(); - try - { - var settings = CreateSettings(requiresMinVer: false, requiresVelopack: false); - var mockDotNet = new Mock(); - - var builder = CreateSilentPipelineBuilder(settings, tempDir, mockDotNet); - var pipeline = await builder.BuildAsync(); - var summary = await pipeline.RunAsync(); - var result = await summary.GetModule(); + using var tempDir = new TempDirectory(); + var settings = CreateSettings(requiresMinVer: false, requiresVelopack: false); + var mockDotNet = new Mock(); - await Assert.That(result.SkipDecisionOrDefault).IsNotNull(); - await Assert.That(result.SkipDecisionOrDefault!.ShouldSkip).IsTrue(); - } - finally - { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + var builder = TestHelpers.CreateSilentPipelineBuilder(new LoomContext(settings, tempDir), + services => + { + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + }); + var pipeline = await builder.BuildAsync(); + var summary = await pipeline.RunAsync(); + var result = await summary.GetModule(); + + await Assert.That(result.SkipDecisionOrDefault).IsNotNull(); + await Assert.That(result.SkipDecisionOrDefault!.ShouldSkip).IsTrue(); } [Test] public async Task ExecuteAsync_CreatesManifest_WhenManifestIsMissing() { - var tempDir = CreateTemporaryDirectory(); - try - { - var settings = CreateSettings(requiresMinVer: true, requiresVelopack: false); - var mockDotNet = new Mock(); + using var tempDir = new TempDirectory(); + var settings = CreateSettings(requiresMinVer: true, requiresVelopack: false); + var mockDotNet = new Mock(); - SetupDotNetMocks(mockDotNet, out var newOptions, out _, out _); + SetupDotNetMocks(mockDotNet, out var newOptions, out _, out _); - var builder = CreateSilentPipelineBuilder(settings, tempDir, mockDotNet); - var pipeline = await builder.BuildAsync(); - await pipeline.RunAsync(); - - await Assert.That(newOptions).Count().IsEqualTo(1); - await Assert.That(newOptions[0].Arguments).IsNotNull(); - await Assert.That(newOptions[0].Arguments!).Contains("tool-manifest"); - } - finally - { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + var builder = TestHelpers.CreateSilentPipelineBuilder(new LoomContext(settings, tempDir), + services => + { + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + }); + var pipeline = await builder.BuildAsync(); + await pipeline.RunAsync(); + + await Assert.That(newOptions).Count().IsEqualTo(1); + await Assert.That(newOptions[0].Arguments).IsNotNull(); + await Assert.That(newOptions[0].Arguments!).Contains("tool-manifest"); } [Test] public async Task ExecuteAsync_DoesNotCreateManifest_WhenManifestExists() { - var tempDir = CreateTemporaryDirectory(); - try - { - // Create a fake manifest - System.IO.File.WriteAllText(Path.Combine(tempDir, "dotnet-tools.json"), "{}"); + using var tempDir = new TempDirectory(); + // Create a fake manifest + System.IO.File.WriteAllText(Path.Combine(tempDir, "dotnet-tools.json"), "{}"); - var settings = CreateSettings(requiresMinVer: true, requiresVelopack: false); - var mockDotNet = new Mock(); + var settings = CreateSettings(requiresMinVer: true, requiresVelopack: false); + var mockDotNet = new Mock(); - SetupDotNetMocks(mockDotNet, out var newOptions, out _, out _); + SetupDotNetMocks(mockDotNet, out var newOptions, out _, out _); - var builder = CreateSilentPipelineBuilder(settings, tempDir, mockDotNet); - var pipeline = await builder.BuildAsync(); - await pipeline.RunAsync(); + var builder = TestHelpers.CreateSilentPipelineBuilder(new LoomContext(settings, tempDir), + services => + { + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + }); + var pipeline = await builder.BuildAsync(); + await pipeline.RunAsync(); - await Assert.That(newOptions).IsEmpty(); - } - finally - { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + await Assert.That(newOptions).IsEmpty(); } // [Test] @@ -267,55 +219,49 @@ public async Task ExecuteAsync_DoesNotCreateManifest_WhenManifestExists() [Test] public async Task ExecuteAsync_RestoresTools_AtEndOfExecution() { - var tempDir = CreateTemporaryDirectory(); - try - { - System.IO.File.WriteAllText(Path.Combine(tempDir, "dotnet-tools.json"), "{}"); + using var tempDir = new TempDirectory(); + System.IO.File.WriteAllText(Path.Combine(tempDir, "dotnet-tools.json"), "{}"); - var settings = CreateSettings(requiresMinVer: true, requiresVelopack: false); - var mockDotNet = new Mock(); + var settings = CreateSettings(requiresMinVer: true, requiresVelopack: false); + var mockDotNet = new Mock(); - SetupDotNetMocks(mockDotNet, out _, out var restoreOptions, out _); + SetupDotNetMocks(mockDotNet, out _, out var restoreOptions, out _); - var builder = CreateSilentPipelineBuilder(settings, tempDir, mockDotNet); - var pipeline = await builder.BuildAsync(); - await pipeline.RunAsync(); + var builder = TestHelpers.CreateSilentPipelineBuilder(new LoomContext(settings, tempDir), + services => + { + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + }); + var pipeline = await builder.BuildAsync(); + await pipeline.RunAsync(); - await Assert.That(restoreOptions).Count().IsEqualTo(1); - } - finally - { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + await Assert.That(restoreOptions).Count().IsEqualTo(1); } [Test] public async Task ExecuteAsync_ReturnsRestoreToolsResult_WrappingCommandResult() { - var tempDir = CreateTemporaryDirectory(); - try - { - System.IO.File.WriteAllText(Path.Combine(tempDir, "dotnet-tools.json"), "{}"); - - var settings = CreateSettings(requiresMinVer: true, requiresVelopack: false); - var mockDotNet = new Mock(); + using var tempDir = new TempDirectory(); + System.IO.File.WriteAllText(Path.Combine(tempDir, "dotnet-tools.json"), "{}"); - SetupDotNetMocks(mockDotNet, out _, out _, out _); + var settings = CreateSettings(requiresMinVer: true, requiresVelopack: false); + var mockDotNet = new Mock(); - var builder = CreateSilentPipelineBuilder(settings, tempDir, mockDotNet); - var pipeline = await builder.BuildAsync(); - var summary = await pipeline.RunAsync(); - var result = await summary.GetModule(); - var val = result.ValueOrDefault; + SetupDotNetMocks(mockDotNet, out _, out _, out _); - await Assert.That(val).IsNotNull(); - await Assert.That(val!.CommandResult).IsNotNull(); // Checking it wrapped it in `RestoreToolsResult` properly - } - finally - { - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + var builder = TestHelpers.CreateSilentPipelineBuilder(new LoomContext(settings, tempDir), + services => + { + services.AddSingleton(mockDotNet.Object); + services.AddModule(); + }); + var pipeline = await builder.BuildAsync(); + var summary = await pipeline.RunAsync(); + var result = await summary.GetModule(); + var val = result.ValueOrDefault; + + await Assert.That(val).IsNotNull(); + await Assert.That(val!.CommandResult).IsNotNull(); // Checking it wrapped it in `RestoreToolsResult` properly } } diff --git a/src/Loom.Build.Tests/Unit/TestFakes.cs b/src/Loom.Build.Tests/Unit/TestFakes.cs new file mode 100644 index 0000000..320aa4e --- /dev/null +++ b/src/Loom.Build.Tests/Unit/TestFakes.cs @@ -0,0 +1,94 @@ +using Loom.MinVer; +using Loom.Modules; + +using ModularPipelines.Context; + +using File = ModularPipelines.FileSystem.File; + +namespace Loom.Build.Tests.Unit; + +public class FakeBuildMinVerModule : MinVerModule +{ + public FakeBuildMinVerModule(LoomContext loomContext) + : base(loomContext) { } + + protected override Task ExecuteAsync( + IModuleContext context, + CancellationToken ct + ) => + Task.FromResult( + new MinVerResult( + new Dictionary + { + [string.Empty] = new MinVerVersion("1.2.3"), + } + ) + ); +} + +public class FakePublishMinVerModule : MinVerModule +{ + public FakePublishMinVerModule(LoomContext loomContext) + : base(loomContext) { } + + protected override Task ExecuteAsync( + IModuleContext context, + CancellationToken ct + ) => + Task.FromResult( + new MinVerResult( + new Dictionary + { + [string.Empty] = new MinVerVersion("1.2.3"), + ["v"] = new MinVerVersion("1.2.4"), + } + ) + ); +} + +public class FakeMinVerModule : MinVerModule +{ + public static readonly MinVerVersion MinVer123 = new("1.2.3"); + public static readonly MinVerVersion MinVer124 = new("1.2.4"); + + public FakeMinVerModule(LoomContext loomContext) + : base(loomContext) { } + + protected override Task ExecuteAsync( + IModuleContext context, + CancellationToken ct + ) => + Task.FromResult( + new MinVerResult( + new Dictionary + { + [string.Empty] = MinVer123, + ["v"] = MinVer124, + } + ) + ); +} + +public class FakeBuildModule : BuildModule +{ + public FakeBuildModule(LoomContext loomContext) + : base(loomContext) { } + + protected override Task ExecuteAsync(IModuleContext context, CancellationToken ct) + { + return Task.FromResult(new BuildResult("success")); + } +} + +public class FakePackModule : PackModule +{ + public FakePackModule(LoomContext buildContext) + : base(buildContext) { } + + protected override Task ExecuteAsync(IModuleContext context, CancellationToken ct) + { + return Task.FromResult( + new PackResult(new List { new File("package1.nupkg"), new File("package2.nupkg") }) + ); + } +} diff --git a/src/Loom.Build.Tests/Unit/TestHelpers.cs b/src/Loom.Build.Tests/Unit/TestHelpers.cs new file mode 100644 index 0000000..c4f0e53 --- /dev/null +++ b/src/Loom.Build.Tests/Unit/TestHelpers.cs @@ -0,0 +1,98 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +using ModularPipelines; +using ModularPipelines.Options; +using ModularPipelines.Models; + +namespace Loom.Build.Tests.Unit; + +public static class TestHelpers +{ + public static FakeTimeProvider DefaultFakeTimeProvider { get; } = new(); + + public static CommandResult EmptyCommandResult(FakeTimeProvider? timeProvider = null) + { + var provider = timeProvider ?? DefaultFakeTimeProvider; + var now = provider.GetUtcNow(); + + return new CommandResult( + "", + "", + "", + "", + new Dictionary(), + now, + now, + TimeSpan.Zero, + 0 + ); + } + + public static PipelineBuilder CreateSilentPipelineBuilder( + LoomContext context, + Action? configureServices = null + ) + { + var builder = Pipeline.CreateBuilder(); + builder.Services.AddSingleton(context); + builder.Services.AddSingleton(new ConfigurationBuilder().Build()); + configureServices?.Invoke(builder.Services); + + builder.Options.PrintLogo = false; + builder.Options.ShowProgressInConsole = false; + builder.Options.PrintResults = false; + builder.Options.PrintDependencyChains = false; + builder.Options.DefaultLoggingOptions = CommandLoggingOptions.Silent; + + return builder; + } +} + +public sealed class FakeTimeProvider : TimeProvider +{ + private DateTimeOffset _utcNow; + + public FakeTimeProvider(DateTimeOffset? initialUtcNow = null) + { + _utcNow = initialUtcNow ?? new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero); + } + + public void SetUtcNow(DateTimeOffset value) + { + _utcNow = value; + } + + public void Advance(TimeSpan delta) + { + _utcNow = _utcNow.Add(delta); + } + + public override DateTimeOffset GetUtcNow() + { + return _utcNow; + } +} + +public sealed class TempDirectory : IDisposable +{ + public string Path { get; } + + public TempDirectory() + { + Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.IO.Path.GetRandomFileName()); + Directory.CreateDirectory(Path); + } + + public void Dispose() + { + if (Directory.Exists(Path)) + { + Directory.Delete(Path, true); + } + } + + public static implicit operator string(TempDirectory d) => d.Path; + + public override string ToString() => Path; +} diff --git a/src/Loom.Build.Tests/Unit/TestModuleTests.cs b/src/Loom.Build.Tests/Unit/TestModuleTests.cs index 6b09cd7..a8ff3bc 100644 --- a/src/Loom.Build.Tests/Unit/TestModuleTests.cs +++ b/src/Loom.Build.Tests/Unit/TestModuleTests.cs @@ -1,13 +1,16 @@ using Loom.Config; using Loom.Modules; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; + using ModularPipelines; using ModularPipelines.DotNet.Options; using ModularPipelines.DotNet.Services; using ModularPipelines.Extensions; using ModularPipelines.Models; using ModularPipelines.Options; + using Moq; namespace Loom.Build.Tests.Unit; @@ -17,112 +20,99 @@ public class TestModuleTests [Test] public async Task ExecuteAsync_CreatesTestResultsDirectory_And_RunsDotNetTest() { - var workingDirectory = CreateTemporaryDirectory(); + var workingDirectory = new TempDirectory(); - try - { - await WriteGlobalJsonAsync(workingDirectory, "Microsoft.Testing.Platform"); - - var mockDotNet = new Mock(); - DotNetTestOptions? capturedOptions = null; - CommandExecutionOptions? capturedExecutionOptions = null; - - mockDotNet - .Setup(x => - x.Test( - It.IsAny(), - It.IsAny(), - It.IsAny() - ) - ) - .Callback( - (options, executionOptions, _) => - { - capturedOptions = options; - capturedExecutionOptions = executionOptions; - } + + await WriteGlobalJsonAsync(workingDirectory, "Microsoft.Testing.Platform"); + + var mockDotNet = new Mock(); + DotNetTestOptions? capturedOptions = null; + CommandExecutionOptions? capturedExecutionOptions = null; + + mockDotNet + .Setup(x => + x.Test( + It.IsAny(), + It.IsAny(), + It.IsAny() ) - .ReturnsAsync((CommandResult)null!); - - var summary = await RunTestModuleAsync(workingDirectory, mockDotNet.Object); - var testModuleResult = await summary.GetModule(); - var resultData = testModuleResult.ValueOrDefault; - var coverageFilePath = Path.Combine(workingDirectory, "TestResults", "coverage.xml"); - - await Assert.That(testModuleResult.IsSuccess).IsTrue(); - await Assert - .That(Directory.Exists(Path.Combine(workingDirectory, "TestResults"))) - .IsTrue(); - await Assert.That(resultData).IsNotNull(); - await Assert.That(resultData!.CoverageFilePath).IsEqualTo(coverageFilePath); - await Assert.That(capturedOptions).IsNotNull(); - await Assert.That(capturedExecutionOptions).IsNotNull(); - await Assert.That(capturedOptions!.Solution).IsEqualTo("test.sln"); - await Assert.That(capturedOptions.Configuration).IsEqualTo("Debug"); - await Assert.That(capturedOptions.NoBuild).IsTrue(); - await Assert.That(capturedOptions.Arguments!).Contains("--coverage"); - await Assert.That(capturedOptions.Arguments!).Contains(coverageFilePath); - await Assert.That(capturedOptions.Arguments!).Contains("xml"); - await Assert - .That(capturedExecutionOptions!.WorkingDirectory) - .IsEqualTo(workingDirectory); - - mockDotNet.Verify( - x => - x.Test( - It.IsAny(), - It.IsAny(), - It.IsAny() - ), - Times.Once - ); - } - finally - { - Directory.Delete(workingDirectory, recursive: true); - } + ) + .Callback( + (options, executionOptions, _) => + { + capturedOptions = options; + capturedExecutionOptions = executionOptions; + } + ) + .ReturnsAsync((CommandResult)null!); + + var summary = await RunTestModuleAsync(workingDirectory, mockDotNet.Object); + var testModuleResult = await summary.GetModule(); + var resultData = testModuleResult.ValueOrDefault; + var coverageFilePath = Path.Combine(workingDirectory, "TestResults", "coverage.xml"); + + await Assert.That(testModuleResult.IsSuccess).IsTrue(); + await Assert + .That(Directory.Exists(Path.Combine(workingDirectory, "TestResults"))) + .IsTrue(); + await Assert.That(resultData).IsNotNull(); + await Assert.That(resultData!.CoverageFilePath).IsEqualTo(coverageFilePath); + await Assert.That(capturedOptions).IsNotNull(); + await Assert.That(capturedExecutionOptions).IsNotNull(); + await Assert.That(capturedOptions!.Solution).IsEqualTo("test.sln"); + await Assert.That(capturedOptions.Configuration).IsEqualTo("Debug"); + await Assert.That(capturedOptions.NoBuild).IsTrue(); + await Assert.That(capturedOptions.Arguments!).Contains("--coverage"); + await Assert.That(capturedOptions.Arguments!).Contains(coverageFilePath); + await Assert.That(capturedOptions.Arguments!).Contains("xml"); + await Assert + .That(capturedExecutionOptions!.WorkingDirectory) + .IsEqualTo(workingDirectory); + + mockDotNet.Verify( + x => + x.Test( + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Once + ); } [Test] public async Task ExecuteAsync_UsesExistingTestResultsDirectory() { - var workingDirectory = CreateTemporaryDirectory(); - - try - { - await WriteGlobalJsonAsync(workingDirectory, "Microsoft.Testing.Platform"); - Directory.CreateDirectory(Path.Combine(workingDirectory, "TestResults")); - - var mockDotNet = new Mock(); - mockDotNet - .Setup(x => - x.Test( - It.IsAny(), - It.IsAny(), - It.IsAny() - ) + using var workingDirectory = new TempDirectory(); + + await WriteGlobalJsonAsync(workingDirectory, "Microsoft.Testing.Platform"); + Directory.CreateDirectory(Path.Combine(workingDirectory, "TestResults")); + + var mockDotNet = new Mock(); + mockDotNet + .Setup(x => + x.Test( + It.IsAny(), + It.IsAny(), + It.IsAny() ) - .ReturnsAsync((CommandResult)null!); - - var summary = await RunTestModuleAsync(workingDirectory, mockDotNet.Object); - var testModuleResult = await summary.GetModule(); - - await Assert.That(testModuleResult.IsSuccess).IsTrue(); - - mockDotNet.Verify( - x => - x.Test( - It.IsAny(), - It.IsAny(), - It.IsAny() - ), - Times.Once - ); - } - finally - { - Directory.Delete(workingDirectory, recursive: true); - } + ) + .ReturnsAsync((CommandResult)null!); + + var summary = await RunTestModuleAsync(workingDirectory, mockDotNet.Object); + var testModuleResult = await summary.GetModule(); + + await Assert.That(testModuleResult.IsSuccess).IsTrue(); + + mockDotNet.Verify( + x => + x.Test( + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Once + ); } [Test] @@ -166,89 +156,76 @@ public void ValidateMicrosoftTestingPlatform_DoesNotThrow_WhenRunnerIsCaseInsens [Test] public async Task ExecuteAsync_Fails_WhenGlobalJsonMissing() { - var workingDirectory = CreateTemporaryDirectory(); + var workingDirectory = new TempDirectory(); + + // No global.json written — module should fail and throw from pipeline + var mockDotNet = new Mock(); + + var exception = Assert.Throws(() => + RunTestModuleAsync(workingDirectory, mockDotNet.Object).GetAwaiter().GetResult() + ); + + await Assert.That(exception!.Message).Contains("global.json not found"); + + mockDotNet.Verify( + x => + x.Test( + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Never + ); - try - { - // No global.json written — module should fail and throw from pipeline - var mockDotNet = new Mock(); - - var exception = Assert.Throws(() => - RunTestModuleAsync(workingDirectory, mockDotNet.Object).GetAwaiter().GetResult() - ); - - await Assert.That(exception!.Message).Contains("global.json not found"); - - mockDotNet.Verify( - x => - x.Test( - It.IsAny(), - It.IsAny(), - It.IsAny() - ), - Times.Never - ); - } - finally - { - Directory.Delete(workingDirectory, recursive: true); - } } [Test] public async Task ExecuteAsync_UsesReleaseConfigurationWhenSet() { - var workingDirectory = CreateTemporaryDirectory(); + var workingDirectory = new TempDirectory(); - try - { - await WriteGlobalJsonAsync(workingDirectory, "Microsoft.Testing.Platform"); - - var mockDotNet = new Mock(); - DotNetTestOptions? capturedOptions = null; - - mockDotNet - .Setup(x => - x.Test( - It.IsAny(), - It.IsAny(), - It.IsAny() - ) - ) - .Callback( - (options, _, _) => capturedOptions = options + await WriteGlobalJsonAsync(workingDirectory, "Microsoft.Testing.Platform"); + + var mockDotNet = new Mock(); + DotNetTestOptions? capturedOptions = null; + + mockDotNet + .Setup(x => + x.Test( + It.IsAny(), + It.IsAny(), + It.IsAny() ) - .ReturnsAsync((CommandResult)null!); + ) + .Callback( + (options, _, _) => capturedOptions = options + ) + .ReturnsAsync((CommandResult)null!); - var settings = new LoomSettings - { - Workspace = new WorkspaceSettings { Solution = "test.sln" }, - Global = new GlobalSettings - { - Target = BuildTarget.Test, - Configuration = "Release", - }, - }; - - var builder = Pipeline.CreateBuilder(); - builder.Services.AddSingleton(new LoomContext(settings, workingDirectory)); - builder.Services.AddSingleton(new ConfigurationBuilder().Build()); - builder.Services.AddSingleton(mockDotNet.Object); - builder.Services.AddModule(); - builder.Options.DefaultLoggingOptions = CommandLoggingOptions.Silent; - - var pipeline = await builder.BuildAsync(); - var summary = await pipeline.RunAsync(); - var testModuleResult = await summary.GetModule(); - - await Assert.That(testModuleResult.IsSuccess).IsTrue(); - await Assert.That(capturedOptions).IsNotNull(); - await Assert.That(capturedOptions!.Configuration).IsEqualTo("Release"); - } - finally + var settings = new LoomSettings { - Directory.Delete(workingDirectory, recursive: true); - } + Workspace = new WorkspaceSettings { Solution = "test.sln" }, + Global = new GlobalSettings + { + Target = BuildTarget.Test, + Configuration = "Release", + }, + }; + + var builder = Pipeline.CreateBuilder(); + builder.Services.AddSingleton(new LoomContext(settings, workingDirectory)); + builder.Services.AddSingleton(new ConfigurationBuilder().Build()); + builder.Services.AddSingleton(mockDotNet.Object); + builder.Services.AddModule(); + builder.Options.DefaultLoggingOptions = CommandLoggingOptions.Silent; + + var pipeline = await builder.BuildAsync(); + var summary = await pipeline.RunAsync(); + var testModuleResult = await summary.GetModule(); + + await Assert.That(testModuleResult.IsSuccess).IsTrue(); + await Assert.That(capturedOptions).IsNotNull(); + await Assert.That(capturedOptions!.Configuration).IsEqualTo("Release"); } private static async Task RunTestModuleAsync( @@ -294,8 +271,4 @@ await File.WriteAllTextAsync( ); } - private static string CreateTemporaryDirectory() - { - return Directory.CreateTempSubdirectory("loom-test-module-").FullName; - } } diff --git a/src/Loom.Build/Commands.cs b/src/Loom.Build/Commands.cs index 5e61b0a..67d0309 100644 --- a/src/Loom.Build/Commands.cs +++ b/src/Loom.Build/Commands.cs @@ -1,7 +1,10 @@ #pragma warning disable CA1822 // Mark members as static using ConsoleAppFramework; + using Loom.Config; + using ModularPipelines; + using Spectre.Console; namespace Loom; @@ -9,42 +12,65 @@ namespace Loom; public class Commands { /// - /// Default command runs loom against loom.json run.target or BuildTarget.Build + /// Default command runs loom against BuildTarget.Build /// - /// Override global rid set in loom.json - /// Build target to run - /// --clean|Prepend Clean target to start of pipeline + /// -f|--clean, Prepend Clean target to start of pipeline /// [Command("")] public async Task Root( CancellationToken ct, - [HideDefaultValue] string? rid = null, - [HideDefaultValue, Argument] BuildTarget? target = null, bool fresh = false ) { - var cliOptions = new GlobalSettings { Rid = rid, Target = target }; + await PipelineRunner.ExecuteAsync(new ExecutionRequest(BuildTarget.Build, null, fresh), ct); + } - var loomPath = LoomConfig.ResolveLoomJsonPath(); + /// + /// Clean project and artifacts. + /// + /// Override global rid set in loom.json + public Task Clean(CancellationToken ct, [HideDefaultValue] string? rid = null) + => PipelineRunner.ExecuteAsync(new ExecutionRequest(BuildTarget.Clean, rid), ct); - if (loomPath == null) - { - AnsiConsole.MarkupLine("[red]Error:[/] loom.json not found."); - AnsiConsole.MarkupLine("Run [yellow]dotnet loom init[/] to get started."); - Environment.Exit(1); - } + /// + /// Restore project dependencies. + /// + /// Override global rid set in loom.json + /// -f|--clean, Prepend Clean target to start of pipeline + public Task Restore(CancellationToken ct, [HideDefaultValue] string? rid = null, bool fresh = false) + => PipelineRunner.ExecuteAsync(new ExecutionRequest(BuildTarget.Restore, rid, fresh), ct); - var builder = Pipeline.CreateBuilder(); - var context = builder.Services.AddLoomContext(loomPath, cliOptions); + /// + /// Build project and artifacts. + /// + /// Override global rid set in loom.json + /// -f|--clean, Prepend Clean target to start of pipeline + public Task Build(CancellationToken ct, [HideDefaultValue] string? rid = null, bool fresh = false) + => PipelineRunner.ExecuteAsync(new ExecutionRequest(BuildTarget.Build, rid, fresh), ct); - builder.Services.AddModules(); - builder.Options.PrintLogo = false; - builder.Options.ShowProgressInConsole = true; - builder.Options.RunOnlyCategories = LoomConfig.GetPipelineCategories(context.Target, fresh); + /// + /// Run tests. + /// + /// Override global rid set in loom.json + /// -f|--clean, Prepend Clean target to start of pipeline + public Task Test(CancellationToken ct, [HideDefaultValue] string? rid = null, bool fresh = false) + => PipelineRunner.ExecuteAsync(new ExecutionRequest(BuildTarget.Test, rid, fresh), ct); - var pipeline = await builder.BuildAsync(); - await pipeline.RunAsync(); - } + /// + /// Build and package project. + /// + /// Override global rid set in loom.json + /// -f|--clean, Prepend Clean target to start of pipeline + public Task Publish(CancellationToken ct, [HideDefaultValue] string? rid = null, bool fresh = false) + => PipelineRunner.ExecuteAsync(new ExecutionRequest(BuildTarget.Publish, rid, fresh), ct); + + /// + /// Build, package, and release project. + /// + /// Override global rid set in loom.json + /// -f|--clean, Prepend Clean target to start of pipeline + public Task Release(CancellationToken ct, [HideDefaultValue] string? rid = null, bool fresh = false) + => PipelineRunner.ExecuteAsync(new ExecutionRequest(BuildTarget.Release, rid, fresh), ct); [Command("init")] public async Task Init(bool force = false) diff --git a/src/Loom.Build/Config/ExecutionOptions.cs b/src/Loom.Build/Config/ExecutionOptions.cs index 383f8f0..7f4b201 100644 --- a/src/Loom.Build/Config/ExecutionOptions.cs +++ b/src/Loom.Build/Config/ExecutionOptions.cs @@ -23,3 +23,9 @@ public class GlobalSettings return dict; } } + +public record ExecutionRequest( + BuildTarget Target, + string? Rid = null, + bool Fresh = false +); diff --git a/src/Loom.Build/Config/LoomContext.cs b/src/Loom.Build/Config/LoomContext.cs index c117139..425b5fa 100644 --- a/src/Loom.Build/Config/LoomContext.cs +++ b/src/Loom.Build/Config/LoomContext.cs @@ -3,6 +3,7 @@ namespace Loom.Config; using System.Collections.ObjectModel; using System.Runtime.InteropServices; +using static Loom.Extensions; public record LoomContext { public LoomContext() { } @@ -35,11 +36,14 @@ public LoomContext(LoomSettings settings, string workingDirectory) || (settings.Workspace.EnableVelopackRelease ?? false); EnableNugetUpload = settings.Workspace.EnableNugetUpload ?? false; EnableGithubRelease = settings.Workspace.EnableGithubRelease ?? false; + DefaultPreReleaseIdentifiers = settings.Workspace.DefaultPreReleaseIdentifiers ?? DefaultPreReleaseIdentifiers_Default; } public string WorkingDirectory { get; init; } = string.Empty; public string Solution { get; init; } = string.Empty; public string ArtifactsDirectory { get; init; } = ".artifacts"; + public string DefaultPreReleaseIdentifiers { get; init; } = DefaultPreReleaseIdentifiers_Default; + public const string DefaultPreReleaseIdentifiers_Default = "preview.0"; public IReadOnlyList CleanDirectories { get; init; } = ["dist"]; public IReadOnlyDictionary Artifacts { get; init; } = @@ -58,14 +62,5 @@ public LoomContext(LoomSettings settings, string workingDirectory) public bool EnableGithubRelease { get; init; } = false; public ReadOnlyCollection ResolvedArtifacts { get; init; } = []; - private static string GetDefaultRid() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return "win-x64"; - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - return RuntimeInformation.ProcessArchitecture == Architecture.Arm64 - ? "osx-arm64" - : "osx-x64"; - return "linux-x64"; - } + } diff --git a/src/Loom.Build/Config/WorkspaceSettings.cs b/src/Loom.Build/Config/WorkspaceSettings.cs index 6a3934b..759b90a 100644 --- a/src/Loom.Build/Config/WorkspaceSettings.cs +++ b/src/Loom.Build/Config/WorkspaceSettings.cs @@ -20,6 +20,8 @@ public class WorkspaceSettings [Description("Whether to create a GitHub release during a release.")] public bool? EnableGithubRelease { get; set; } = false; + [Description("Maps to MinVer.DefaultPreReleaseIdentifiers")] + public string? DefaultPreReleaseIdentifiers { get; set; } = LoomContext.DefaultPreReleaseIdentifiers_Default; [Description("Whether to create velopack packages during a release.")] public bool? EnableVelopackRelease { get; set; } = false; diff --git a/src/Loom.Build/Extensions.cs b/src/Loom.Build/Extensions.cs index 99e87e4..c22f068 100644 --- a/src/Loom.Build/Extensions.cs +++ b/src/Loom.Build/Extensions.cs @@ -109,20 +109,15 @@ public static bool IsNativeHostCompatible(string targetRid) } public static string GetDefaultRid() { - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return "linux-x64"; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return "osx-x64"; - } - else - { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return "win-x64"; - } + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return RuntimeInformation.ProcessArchitecture == Architecture.Arm64 + ? "osx-arm64" + : "osx-x64"; + return "linux-x64"; } + } extension(IConfiguration configuration) { } diff --git a/src/Loom.Build/Modules/BuildModule.cs b/src/Loom.Build/Modules/BuildModule.cs index 46d9b3f..f8e9207 100644 --- a/src/Loom.Build/Modules/BuildModule.cs +++ b/src/Loom.Build/Modules/BuildModule.cs @@ -6,6 +6,7 @@ public record BuildResult(string? Output); [ModuleCategory("Build")] [DependsOn(Optional = true)] +[DependsOn(Optional = true)] public class BuildModule(LoomContext buildContext) : Module { protected override async Task ExecuteAsync( @@ -13,6 +14,12 @@ public class BuildModule(LoomContext buildContext) : Module CancellationToken ct ) { + var minVerModule = await context.GetModule(); + var minVerResult = minVerModule.ValueOrDefault; + var versionProperties = PublishHelpers.CreateVersionProperties( + PublishHelpers.ResolveVersion(minVerResult) + ); + var result = await context .DotNet() .Build( @@ -21,6 +28,7 @@ CancellationToken ct ProjectSolution = buildContext.Solution, NoRestore = true, Configuration = buildContext.Configuration, + Properties = versionProperties, }, executionOptions: new CommandExecutionOptions { diff --git a/src/Loom.Build/Modules/GitHubReleaseModule.cs b/src/Loom.Build/Modules/GitHubReleaseModule.cs index 2664ded..fb70e25 100644 --- a/src/Loom.Build/Modules/GitHubReleaseModule.cs +++ b/src/Loom.Build/Modules/GitHubReleaseModule.cs @@ -1,5 +1,7 @@ using Loom.Config; + using ModularPipelines.GitHub.Extensions; + using Octokit; namespace Loom.Modules; @@ -100,37 +102,36 @@ CancellationToken ct } var files = folder.GetFiles(f => true); - foreach (var file in files) - { - var fileName = file.Name; - if (fileName.StartsWith("assets.", StringComparison.OrdinalIgnoreCase)) - continue; - - var existingAsset = release.Assets.FirstOrDefault(a => - a.Name.Equals(fileName, StringComparison.OrdinalIgnoreCase) - ); + var uploadTasks = files.Where(f => !f.Name.StartsWith("assets", StringComparison.OrdinalIgnoreCase)) + .Select(async file => + { + var existingAsset = release.Assets.FirstOrDefault(a => a.Name.Equals(file, StringComparison.OrdinalIgnoreCase)); if (existingAsset != null) { context.Logger.LogInformation( "Asset {FileName} already exists. Deleting prior asset...", - fileName + file ); await client.Repository.Release.DeleteAsset(owner, repo, existingAsset.Id); } - context.Logger.LogInformation("Uploading asset {FileName}...", fileName); + context.Logger.LogInformation("Uploading asset {FileName}...", file); await using var stream = file.GetStream(); var assetUpload = new ReleaseAssetUpload { - FileName = fileName, + FileName = file, ContentType = "application/octet-stream", RawData = stream, }; await client.Repository.Release.UploadAsset(release, assetUpload, ct); - } + + }); + + await Task.WhenAll(uploadTasks); + context.Logger.LogInformation( "Successfully uploaded {Count} assets to GitHub Release.", files.Count() diff --git a/src/Loom.Build/Modules/MinVerModule.cs b/src/Loom.Build/Modules/MinVerModule.cs index d873f95..49ebfe3 100644 --- a/src/Loom.Build/Modules/MinVerModule.cs +++ b/src/Loom.Build/Modules/MinVerModule.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; + using Loom.Config; using Loom.MinVer; using Loom.MinVer.Options; @@ -22,27 +23,18 @@ CancellationToken ct { var prefixes = loomContext .Artifacts.Values.Select(a => a.TagPrefix ?? string.Empty) - .Distinct() - .ToList(); - - if (!prefixes.Contains(string.Empty)) - { - prefixes.Add(string.Empty); - } + .Append(string.Empty) + .Distinct(); - var results = new ConcurrentDictionary(); - await Task.WhenAll( - prefixes.Select(async prefix => - { - var tagPrefix = string.IsNullOrEmpty(prefix) ? null : prefix; - var options = new DotNetMinVerOptions() { TagPrefix = tagPrefix }; - var version = await context.MinVer().Run(options); - - results[prefix] = version; - }) - ); + var tasks = prefixes.Select(async prefix => + { + var tagPrefix = string.IsNullOrEmpty(prefix) ? null : prefix; + var version = await context.MinVer().Run(new DotNetMinVerOptions { TagPrefix = tagPrefix, DefaultPreReleaseIdentifiers = loomContext.DefaultPreReleaseIdentifiers }); + return KeyValuePair.Create(prefix, version); + }); + var results = await Task.WhenAll(tasks); - return new MinVerResult(results); + return new MinVerResult(results.ToDictionary()); } } diff --git a/src/Loom.Build/Modules/PackModule.cs b/src/Loom.Build/Modules/PackModule.cs index bdb2b8a..2bf0ec2 100644 --- a/src/Loom.Build/Modules/PackModule.cs +++ b/src/Loom.Build/Modules/PackModule.cs @@ -1,6 +1,7 @@ using Loom.Config; -using Loom.MinVer; + using ModularPipelines.FileSystem; + using File = ModularPipelines.FileSystem.File; using SearchOption = System.IO.SearchOption; @@ -49,15 +50,8 @@ CancellationToken ct artifactSettings.Project ); - var version = !string.IsNullOrWhiteSpace(artifactSettings.Version) - ? MinVerVersion.From(artifactSettings.Version) - : minVerResult?.GetVersion(artifactSettings.TagPrefix); - - var properties = new List(); - if (!string.IsNullOrWhiteSpace(version!.ToString())) - { - properties.Add(new("Version", version.ToString())); - } + var version = PublishHelpers.ResolveVersion(artifactSettings, minVerResult); + var properties = PublishHelpers.CreateVersionProperties(version); await context .DotNet() @@ -68,7 +62,7 @@ await context Configuration = buildContext.Configuration, Output = outputDir, NoBuild = true, - Properties = properties.Count > 0 ? properties : null, + Properties = properties, }, executionOptions: new CommandExecutionOptions { diff --git a/src/Loom.Build/Modules/PublishHelpers.cs b/src/Loom.Build/Modules/PublishHelpers.cs index bbd721c..1f826e3 100644 --- a/src/Loom.Build/Modules/PublishHelpers.cs +++ b/src/Loom.Build/Modules/PublishHelpers.cs @@ -1,9 +1,29 @@ -using System.Runtime.InteropServices; +using Loom.Config; +using Loom.MinVer; namespace Loom.Modules; public static class PublishHelpers { + public static MinVerVersion ResolveVersion(ArtifactSettings artifact, MinVerResult? minVerResult) + { + if (!string.IsNullOrWhiteSpace(artifact.Version)) + { + return MinVerVersion.From(artifact.Version)!; + } + return minVerResult?.GetVersion(artifact.TagPrefix) ?? MinVerVersion.V1; + } + public static MinVerVersion ResolveVersion(MinVerResult? minVerResult) => + minVerResult?.GetVersion(null) ?? MinVerVersion.V1; + + public static List CreateVersionProperties(MinVerVersion version) => + [ + new("AssemblyVersion", version.AssemblyVersion), + new("FileVersion", version.FileVersion), + new("InformationalVersion", version.Version), + new("PackageVersion", version.PackageVersion), + new("Version", version.Version), + ]; } diff --git a/src/Loom.Build/Modules/PublishModule.cs b/src/Loom.Build/Modules/PublishModule.cs index b2617e8..470b2ac 100644 --- a/src/Loom.Build/Modules/PublishModule.cs +++ b/src/Loom.Build/Modules/PublishModule.cs @@ -16,6 +16,7 @@ public record PublishResult(List Artifacts); [ModuleCategory("Packaging")] [DependsOn(Optional = true)] [DependsOn(Optional = true)] +[DependsOn(Optional = true)] public class PublishModule(LoomContext buildContext) : Module { private async Task IsAotEnabled(string projectPath, IModuleContext context) @@ -46,6 +47,9 @@ protected override ModuleConfiguration Configure() CancellationToken ct ) { + var minVerModule = await context.GetModule(); + var minVerResult = minVerModule.ValueOrDefault; + var publishableArtifacts = buildContext.ResolvedArtifacts .Where(a => a.CanBuildOnHost && (a.Settings.Type == ArtifactType.Executable || a.Settings.Type == ArtifactType.Velopack)); @@ -82,6 +86,10 @@ CancellationToken ct buildContext.Configuration ); + var versionProperties = PublishHelpers.CreateVersionProperties( + PublishHelpers.ResolveVersion(artifact.Settings, minVerResult) + ); + await context .DotNet() .Publish( @@ -91,6 +99,7 @@ await context Configuration = buildContext.Configuration, Output = publishFolder.Path, Runtime = artifact.Rid, + Properties = versionProperties, }, executionOptions: new CommandExecutionOptions { diff --git a/src/Loom.Build/Modules/VelopackReleaseModule.cs b/src/Loom.Build/Modules/VelopackReleaseModule.cs index fc6d6a8..5f5d988 100644 --- a/src/Loom.Build/Modules/VelopackReleaseModule.cs +++ b/src/Loom.Build/Modules/VelopackReleaseModule.cs @@ -76,29 +76,28 @@ CancellationToken ct artifact.Rid ); - VelopackPackBaseOptions velopackPackOptions = new() + VelopackPackBaseOptions velopackPackOptions = artifact.Rid.ToLower() switch { - PackId = packId, - PackVersion = version.ToString(), - PackDir = publishDir, - OutputDir = releaseDir, - Runtime = artifact.Rid, - }; - - velopackPackOptions = artifact.Rid.ToLower() switch - { - var r when r.StartsWith("win") => new DotNetVelopackPackWinOptions() with + var r when r.StartsWith("win") => new DotNetVelopackPackWinOptions { - PackId = velopackPackOptions.PackId, - PackVersion = velopackPackOptions.PackVersion, - PackDir = velopackPackOptions.PackDir, - OutputDir = velopackPackOptions.OutputDir, - Runtime = velopackPackOptions.Runtime, - Shortcuts = "None", + PackId = packId, + PackVersion = version.ToString(), + PackDir = publishDir, + OutputDir = releaseDir, + Runtime = artifact.Rid, + Shortcuts = "None" }, - var r when r.StartsWith("linux") => velopackPackOptions, - _ => throw new NotSupportedException("Switch case not supported"), + var r when r.StartsWith("linux") => new DotNetVelopackPackLinuxOptions + { + PackId = packId, + PackVersion = version.ToString(), + PackDir = publishDir, + OutputDir = releaseDir, + Runtime = artifact.Rid + }, + _ => throw new NotSupportedException($"Velopack packaging not supported for {artifact.Rid}") }; + await context .Velopack() .ExecuteAsync( diff --git a/src/Loom.Build/PipelineRunner.cs b/src/Loom.Build/PipelineRunner.cs new file mode 100644 index 0000000..5ff29b6 --- /dev/null +++ b/src/Loom.Build/PipelineRunner.cs @@ -0,0 +1,35 @@ +using Loom.Config; + +using ModularPipelines; + +using Spectre.Console; + +namespace Loom; + +public static class PipelineRunner +{ + public static async Task ExecuteAsync(ExecutionRequest request, CancellationToken ct) + { + var cliOptions = new GlobalSettings { Rid = request.Rid, Target = request.Target }; + + var loomPath = LoomConfig.ResolveLoomJsonPath(); + + if (loomPath is null) + { + AnsiConsole.MarkupLine("[red]Error:[/] loom.json not found."); + AnsiConsole.MarkupLine("Run [yellow]dotnet loom init[/] to get started."); + Environment.Exit(1); + } + + var builder = Pipeline.CreateBuilder(); + var context = builder.Services.AddLoomContext(loomPath, cliOptions); + + builder.Services.AddModules(); + builder.Options.PrintLogo = false; + builder.Options.ShowProgressInConsole = true; + builder.Options.RunOnlyCategories = LoomConfig.GetPipelineCategories(context.Target, request.Fresh); + + var pipeline = await builder.BuildAsync(); + await pipeline.RunAsync(ct); + } +} diff --git a/src/Loom.Build/Properties/launchSettings.json b/src/Loom.Build/Properties/launchSettings.json index 3557ce6..b27a190 100644 --- a/src/Loom.Build/Properties/launchSettings.json +++ b/src/Loom.Build/Properties/launchSettings.json @@ -3,7 +3,7 @@ "loom.example": { "commandName": "Project", "commandLineArgs": "release", - "workingDirectory": "V:/source/personal/typical", + "workingDirectory": "C:/source/personal/typical", } } } diff --git a/src/Loom.Build/Setup.cs b/src/Loom.Build/Setup.cs index 6d0ed94..d586d82 100644 --- a/src/Loom.Build/Setup.cs +++ b/src/Loom.Build/Setup.cs @@ -1,7 +1,10 @@ using System.Text.Json; + using Loom.Config; + using NJsonSchema; using NJsonSchema.Generation; + using Spectre.Console; namespace Loom; @@ -140,12 +143,11 @@ bool force Global = new GlobalSettings(), }; - var jsonContent = JsonSerializer.Serialize(loom, LoomSettingsContext.Default.LoomSettings); - var finalJson = $$""" -{ - "$schema": "./loom.schema.json",{{jsonContent[1..]}} -"""; + var jsonNode = JsonSerializer.SerializeToNode(loom, LoomSettingsContext.Default.LoomSettings)!.AsObject(); + jsonNode.Insert(0, "$schema", "./loom.schema.json"); // .Insert() is available in .NET 8+ + + var finalJson = jsonNode.ToJsonString(new JsonSerializerOptions { WriteIndented = true }); var destinationLoomFile = Path.Combine(buildDir, "loom.json"); diff --git a/src/Loom.Build/loom.schema.json.example b/src/Loom.Build/loom.schema.json.example index e8f8d24..4e5a842 100644 --- a/src/Loom.Build/loom.schema.json.example +++ b/src/Loom.Build/loom.schema.json.example @@ -69,6 +69,10 @@ "type": "boolean", "description": "Whether to create a GitHub release during a release." }, + "defaultPreReleaseIdentifiers": { + "type": "string", + "description": "Maps to MinVer.DefaultPreReleaseIdentifiers" + }, "enableVelopackRelease": { "type": "boolean", "description": "Whether to create velopack packages during a release."