diff --git a/src/Fallout.Migrate/Steps/RewriteBootstrapScriptsStep.cs b/src/Fallout.Migrate/Steps/RewriteBootstrapScriptsStep.cs index 1e1fb5df..741b118b 100644 --- a/src/Fallout.Migrate/Steps/RewriteBootstrapScriptsStep.cs +++ b/src/Fallout.Migrate/Steps/RewriteBootstrapScriptsStep.cs @@ -1,3 +1,4 @@ +using System.Text.RegularExpressions; using System.Threading.Tasks; using Fallout.Common.IO; using Fallout.Migrate.Common; @@ -5,11 +6,30 @@ namespace Fallout.Migrate.Steps; /// -/// Rewrites build.cmd, build.ps1, and build.sh at the repository root, when -/// present, via . +/// Rewrites bootstrap scripts (build.cmd/build.ps1/build.sh): dotnet nuke +/// invocations, .nuke path references, and legacy NUKE_* environment variables become +/// their Fallout equivalents. /// internal sealed class RewriteBootstrapScriptsStep : IMigrationStep { + /// The ordered find/replace patterns applied by . + private static readonly (Regex Pattern, string Replacement)[] patterns = + [ + // Strip any telemetry opt-out line entirely — telemetry was removed from Fallout + // (ADR-0010), so there is nothing to opt out of. Handles bash `export`, PowerShell + // `$env:`, and cmd `set` spellings by matching the whole line. Runs before the + // env-var renames below so the line is gone rather than renamed to a dead variable. + (new Regex(@"^.*\b(?:NUKE|FALLOUT)_TELEMETRY_OPTOUT\b.*\r?\n?", RegexOptions.Compiled | RegexOptions.Multiline), ""), + // `dotnet nuke` invocations + (new Regex(@"\bdotnet\s+nuke\b", RegexOptions.Compiled), "dotnet fallout"), + // .nuke directory references → .fallout + (new Regex(@"(?<=[\\/.""'\s])\.nuke(?=[\\/""'\s])", RegexOptions.Compiled), ".fallout"), + // Legacy env vars (consumer-facing ones from P3.5c) + (new Regex(@"\bNUKE_GLOBAL_TOOL_VERSION\b", RegexOptions.Compiled), "FALLOUT_GLOBAL_TOOL_VERSION"), + (new Regex(@"\bNUKE_GLOBAL_TOOL_START_TIME\b", RegexOptions.Compiled), "FALLOUT_GLOBAL_TOOL_START_TIME"), + (new Regex(@"\bNUKE_INTERNAL_INTERCEPTOR\b", RegexOptions.Compiled), "FALLOUT_INTERNAL_INTERCEPTOR") + ]; + /// public Task ExecuteAsync(MigrationContext context, Summary summary) { @@ -23,10 +43,32 @@ public Task ExecuteAsync(MigrationContext context, Summary summary) var path = context.RootDirectory / name; if (path.FileExists()) { - MigrationFileOperations.ApplyRewrite(context, path, ScriptRewriter.Rewrite, summary); + MigrationFileOperations.ApplyRewrite(context, path, Rewrite, summary); } } return Task.CompletedTask; } + + /// + /// Rewrites script content, applying every pattern in + /// in order. + /// + /// The original script file content. + /// The rewritten content and the number of edits made. + private static RewriteResult Rewrite(string original) + { + var edits = 0; + var content = original; + foreach (var (pattern, replacement) in patterns) + { + content = pattern.Replace(content, _ => + { + edits++; + return replacement; + }); + } + + return new RewriteResult(content, edits); + } } diff --git a/src/Fallout.Migrate/Steps/RewriteCsFilesStep.cs b/src/Fallout.Migrate/Steps/RewriteCsFilesStep.cs index 7c0bfbd7..2b6064b1 100644 --- a/src/Fallout.Migrate/Steps/RewriteCsFilesStep.cs +++ b/src/Fallout.Migrate/Steps/RewriteCsFilesStep.cs @@ -49,7 +49,7 @@ public Task ExecuteAsync(MigrationContext context, Summary summary) /// /// The original .cs file content. /// The rewritten content and the number of edits made. - public static RewriteResult Rewrite(string original) + private static RewriteResult Rewrite(string original) { var edits = 0; diff --git a/src/Fallout.Migrate/Steps/RewriteCsprojsStep.cs b/src/Fallout.Migrate/Steps/RewriteCsprojsStep.cs index 37e98152..1d211826 100644 --- a/src/Fallout.Migrate/Steps/RewriteCsprojsStep.cs +++ b/src/Fallout.Migrate/Steps/RewriteCsprojsStep.cs @@ -80,7 +80,7 @@ public Task ExecuteAsync(MigrationContext context, Summary summary) /// The original .csproj file content. /// The Fallout version to pin into rewritten inline-versioned references. /// The rewritten content and the number of edits made. - public static RewriteResult Rewrite(string original, string falloutVersion) + private static RewriteResult Rewrite(string original, string falloutVersion) { var edits = 0; var content = original; diff --git a/src/Fallout.Migrate/Steps/ScriptRewriter.cs b/src/Fallout.Migrate/Steps/ScriptRewriter.cs deleted file mode 100644 index 2e681a3c..00000000 --- a/src/Fallout.Migrate/Steps/ScriptRewriter.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Text.RegularExpressions; -using Fallout.Migrate.Common; - -namespace Fallout.Migrate.Steps; - -/// -/// Rewrites bootstrap scripts (build.cmd/build.ps1/build.sh): dotnet nuke -/// invocations, .nuke path references, and legacy NUKE_* environment variables become -/// their Fallout equivalents. Driven by . -/// -internal static class ScriptRewriter -{ - /// The ordered find/replace patterns applied by . - private static readonly (Regex Pattern, string Replacement)[] patterns = - { - // Strip any telemetry opt-out line entirely — telemetry was removed from Fallout - // (ADR-0010), so there is nothing to opt out of. Handles bash `export`, PowerShell - // `$env:`, and cmd `set` spellings by matching the whole line. Runs before the - // env-var renames below so the line is gone rather than renamed to a dead variable. - (new Regex(@"^.*\b(?:NUKE|FALLOUT)_TELEMETRY_OPTOUT\b.*\r?\n?", RegexOptions.Compiled | RegexOptions.Multiline), ""), - // `dotnet nuke` invocations - (new Regex(@"\bdotnet\s+nuke\b", RegexOptions.Compiled), "dotnet fallout"), - // .nuke directory references → .fallout - (new Regex(@"(?<=[\\/.""'\s])\.nuke(?=[\\/""'\s])", RegexOptions.Compiled), ".fallout"), - // Legacy env vars (consumer-facing ones from P3.5c) - (new Regex(@"\bNUKE_GLOBAL_TOOL_VERSION\b", RegexOptions.Compiled), "FALLOUT_GLOBAL_TOOL_VERSION"), - (new Regex(@"\bNUKE_GLOBAL_TOOL_START_TIME\b", RegexOptions.Compiled), "FALLOUT_GLOBAL_TOOL_START_TIME"), - (new Regex(@"\bNUKE_INTERNAL_INTERCEPTOR\b", RegexOptions.Compiled), "FALLOUT_INTERNAL_INTERCEPTOR"), - }; - - /// - /// Rewrites script content, applying every pattern in - /// in order. - /// - /// The original script file content. - /// The rewritten content and the number of edits made. - public static RewriteResult Rewrite(string original) - { - var edits = 0; - var content = original; - foreach (var (pattern, replacement) in patterns) - { - content = pattern.Replace(content, _ => - { - edits++; - return replacement; - }); - } - - return new RewriteResult(content, edits); - } -} diff --git a/tests/Fallout.Migrate.Specs/MigrationIntegrationSpecs.cs b/tests/Fallout.Migrate.Specs/MigrationIntegrationSpecs.cs index 39cb6108..995a23ad 100644 --- a/tests/Fallout.Migrate.Specs/MigrationIntegrationSpecs.cs +++ b/tests/Fallout.Migrate.Specs/MigrationIntegrationSpecs.cs @@ -51,6 +51,12 @@ class Build : NukeBuild #!/usr/bin/env bash export NUKE_TELEMETRY_OPTOUT=1 TEMP_DIRECTORY="$SCRIPT_DIR/.nuke/temp" + + if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "$NUKE_ENTERPRISE_TOKEN" != "" ]]; then + "$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true + "$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true + fi + dotnet nuke "$@" """); diff --git a/tests/Fallout.Migrate.Specs/RewriteBootstrapScriptsStepSpecs.cs b/tests/Fallout.Migrate.Specs/RewriteBootstrapScriptsStepSpecs.cs new file mode 100644 index 00000000..4c847fc8 --- /dev/null +++ b/tests/Fallout.Migrate.Specs/RewriteBootstrapScriptsStepSpecs.cs @@ -0,0 +1,198 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Fallout.Common.IO; +using Fallout.Migrate.Common; +using Fallout.Migrate.Steps; +using FluentAssertions; +using Xunit; + +namespace Fallout.Migrate.Specs; + +public class RewriteBootstrapScriptsStepSpecs : IDisposable +{ + private readonly AbsolutePath tempDirectory; + private readonly MigrationContext context; + private readonly Summary summary = new(); + + public RewriteBootstrapScriptsStepSpecs() + { + tempDirectory = AbsolutePath.Temp("fallout-migrate-test"); + context = new MigrationContext(tempDirectory, dryRun: false, TextWriter.Null); + } + + public void Dispose() + { + tempDirectory.DeleteDirectory(); + } + + [Fact] + public async Task Dotnet_nuke_invocations_become_dotnet_fallout() + { + (tempDirectory / "build.sh").WriteAllText("dotnet nuke Compile", eofLineBreak: false); + + await new RewriteBootstrapScriptsStep().ExecuteAsync(context, summary); + + summary.EditCount.Should().Be(1); + var buildSh = (tempDirectory / "build.sh").ReadAllText(); + buildSh.Should().Be("dotnet fallout Compile"); + } + + [Fact] + public async Task Dot_nuke_directory_references_become_dot_fallout() + { + (tempDirectory / "build.sh").WriteAllText("""TEMP_DIRECTORY="$SCRIPT_DIR/.nuke/temp" """, eofLineBreak: false); + + await new RewriteBootstrapScriptsStep().ExecuteAsync(context, summary); + + summary.EditCount.Should().Be(1); + var buildSh = (tempDirectory / "build.sh").ReadAllText(); + buildSh.Should().Contain(".fallout/temp"); + buildSh.Should().NotContain(".nuke/"); + } + + [Fact] + public async Task Legacy_nuke_env_vars_are_renamed_to_their_fallout_equivalents() + { + (tempDirectory / "build.ps1").WriteAllText(""" + $env:NUKE_GLOBAL_TOOL_VERSION = "10.0" + """, eofLineBreak: false); + + await new RewriteBootstrapScriptsStep().ExecuteAsync(context, summary); + + summary.EditCount.Should().Be(1); + var buildPs1 = (tempDirectory / "build.ps1").ReadAllText(); + buildPs1.Should().Contain("FALLOUT_GLOBAL_TOOL_VERSION"); + } + + [Theory] + [InlineData("build.sh", "export NUKE_TELEMETRY_OPTOUT=1")] // build.sh + [InlineData("build.ps1", """$env:NUKE_TELEMETRY_OPTOUT = "1" """)] // build.ps1 + [InlineData("build.cmd", "set NUKE_TELEMETRY_OPTOUT=1")] // build.cmd + public async Task Telemetry_opt_out_line_is_stripped_entirely(string fileName, string optOutLine) + { + // Telemetry was removed from Fallout (ADR-0010) — the opt-out is dropped, not renamed + // to a dead FALLOUT_TELEMETRY_OPTOUT. Whichever bootstrap script spelled it, the whole + // line goes and the surrounding ones are untouched. + var input = $""" + export DOTNET_ROLL_FORWARD="Major" + {optOutLine} + dotnet nuke "$@" + """; + (tempDirectory / fileName).WriteAllText(input, eofLineBreak: false); + + await new RewriteBootstrapScriptsStep().ExecuteAsync(context, summary); + + var content = (tempDirectory / fileName).ReadAllText(); + content.Should().NotContain("TELEMETRY_OPTOUT"); + content.Should().Contain("DOTNET_ROLL_FORWARD"); + content.Should().Contain("dotnet fallout"); + } + + [Fact] + public async Task Plain_word_nuke_in_prose_is_left_alone() + { + // The word "nuke" in a comment or string isn't a command invocation. + (tempDirectory / "build.sh").WriteAllText("# This was previously a NUKE-based build.", eofLineBreak: false); + + await new RewriteBootstrapScriptsStep().ExecuteAsync(context, summary); + + summary.EditCount.Should().Be(0); + } + + [Fact] + public async Task Removes_Nuke_enterprise_unix_bootstrapper_leftovers() + { + (tempDirectory / "build.sh").WriteAllText(""" + echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" + + if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "$NUKE_ENTERPRISE_TOKEN" != "" ]]; then + "$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true + "$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true + fi + + "$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet + "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" + """, eofLineBreak: false); + + await new CleanupBootstrapScriptsStep().ExecuteAsync(context, summary); + + var buildSh = (tempDirectory / "build.sh").ReadAllText(); + summary.EditCount.Should().Be(1); + buildSh.Should().Be( + """ + echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" + + "$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet + "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" + """); + } + + [Fact] + public async Task Does_not_throw_when_Nuke_enterprise_check_is_last_in_file() + { + (tempDirectory / "build.sh").WriteAllText(""" + if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "$NUKE_ENTERPRISE_TOKEN" != "" ]]; then + "$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true + "$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true + fi + """); + + await new CleanupBootstrapScriptsStep().ExecuteAsync(context, summary); + + var buildSh = (tempDirectory / "build.sh").ReadAllText(); + summary.EditCount.Should().Be(1); + buildSh.Should().Be(""); + } + + [Fact] + public async Task Removes_Nuke_enterprise_windows_bootstrapper_leftovers() + { + (tempDirectory / "build.ps1").WriteAllText(""" + Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" + + if (Test-Path env:NUKE_ENTERPRISE_TOKEN) { + & $env:DOTNET_EXE nuget remove source "nuke-enterprise" > $null + & $env:DOTNET_EXE nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password $env:NUKE_ENTERPRISE_TOKEN > $null + } + + ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } + ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } + """, eofLineBreak: false); + + await new CleanupBootstrapScriptsStep().ExecuteAsync(context, summary); + + var buildPs1 = (tempDirectory / "build.ps1").ReadAllText(); + summary.EditCount.Should().Be(1); + buildPs1.Should().Be( + """ + Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" + + ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } + ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } + """); + } + + [Fact] + public async Task Leaves_bootstrapper_scripts_without_leftovers_alone() + { + (tempDirectory / "build.sh").WriteAllText(""" + echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" + + "$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet + "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" + """, eofLineBreak: false); + + await new CleanupBootstrapScriptsStep().ExecuteAsync(context, summary); + + var buildSh = (tempDirectory / "build.sh").ReadAllText(); + summary.EditCount.Should().Be(0); + buildSh.Should().Be( + """ + echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" + + "$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet + "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" + """); + } +} diff --git a/tests/Fallout.Migrate.Specs/RewriteCsFilesStepSpecs.cs b/tests/Fallout.Migrate.Specs/RewriteCsFilesStepSpecs.cs index 988cb16c..c98925c6 100644 --- a/tests/Fallout.Migrate.Specs/RewriteCsFilesStepSpecs.cs +++ b/tests/Fallout.Migrate.Specs/RewriteCsFilesStepSpecs.cs @@ -1,111 +1,160 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Fallout.Common.IO; +using Fallout.Migrate.Common; +using Fallout.Migrate.Steps; using FluentAssertions; using Xunit; -using Fallout.Migrate.Steps; namespace Fallout.Migrate.Specs; -public class RewriteCsFilesStepSpecs +public class RewriteCsFilesStepSpecs : IDisposable { + private readonly AbsolutePath tempDirectory; + private readonly MigrationContext context; + private readonly Summary summary = new(); + + public RewriteCsFilesStepSpecs() + { + tempDirectory = AbsolutePath.Temp("fallout-migrate-test"); + context = new MigrationContext(tempDirectory, dryRun: false, TextWriter.Null); + } + + public void Dispose() + { + tempDirectory.DeleteDirectory(); + } + [Fact] - public void RewritesUsingDirective() + public async Task Nuke_using_directives_are_rewritten_to_fallout() { - const string input = """ - using Nuke.Common; - using Nuke.Common.IO; - using Fallout.Common; - """; + (tempDirectory / "Build.cs").WriteAllText(""" + using Nuke.Common; + using Nuke.Common.IO; + using Fallout.Common; + """); - var result = RewriteCsFilesStep.Rewrite(input); + await new RewriteCsFilesStep().ExecuteAsync(context, summary); - result.EditCount.Should().Be(2); - result.Content.Should().Contain("using Fallout.Common;"); - result.Content.Should().Contain("using Fallout.Common.IO;"); + summary.EditCount.Should().Be(2); + var buildCs = (tempDirectory / "Build.cs").ReadAllText(); + buildCs.Should().Contain("using Fallout.Common;"); + buildCs.Should().Contain("using Fallout.Common.IO;"); } [Fact] - public void RewritesQualifiedTypeReference() + public async Task Qualified_nuke_type_references_are_rewritten_to_fallout() { - const string input = "var x = new Nuke.Common.Tools.DotNet.DotNetTasks();"; - var result = RewriteCsFilesStep.Rewrite(input); - result.EditCount.Should().Be(1); - result.Content.Should().Be("var x = new Fallout.Common.Tools.DotNet.DotNetTasks();"); + (tempDirectory / "Build.cs").WriteAllText("var x = new Nuke.Common.Tools.DotNet.DotNetTasks();"); + + await new RewriteCsFilesStep().ExecuteAsync(context, summary); + + summary.EditCount.Should().Be(1); + var buildCs = (tempDirectory / "Build.cs").ReadAllText().Trim(); + buildCs.Should().Be("var x = new Fallout.Common.Tools.DotNet.DotNetTasks();"); } [Fact] - public void RewritesNukeBuildBaseType() + public async Task NukeBuild_base_type_is_renamed_to_FalloutBuild() { - const string input = "class Build : NukeBuild { }"; - var result = RewriteCsFilesStep.Rewrite(input); - result.EditCount.Should().Be(1); - result.Content.Should().Be("class Build : FalloutBuild { }"); + (tempDirectory / "Build.cs").WriteAllText("class Build : NukeBuild { }"); + + await new RewriteCsFilesStep().ExecuteAsync(context, summary); + + summary.EditCount.Should().Be(1); + var buildCs = (tempDirectory / "Build.cs").ReadAllText().Trim(); + buildCs.Should().Be("class Build : FalloutBuild { }"); } [Fact] - public void RewritesINukeBuildInterface() + public async Task INukeBuild_interface_is_renamed_to_IFalloutBuild() { - const string input = "public static int IsApplicable(INukeBuild build) => 0;"; - var result = RewriteCsFilesStep.Rewrite(input); - result.EditCount.Should().Be(1); - result.Content.Should().Be("public static int IsApplicable(IFalloutBuild build) => 0;"); + (tempDirectory / "Build.cs").WriteAllText("public static int IsApplicable(INukeBuild build) => 0;"); + + await new RewriteCsFilesStep().ExecuteAsync(context, summary); + + summary.EditCount.Should().Be(1); + var buildCs = (tempDirectory / "Build.cs").ReadAllText().Trim(); + buildCs.Should().Be("public static int IsApplicable(IFalloutBuild build) => 0;"); } [Fact] - public void DoesNotMatchNukeAsPartOfAnotherIdentifier() + public async Task Nuke_as_part_of_another_identifier_is_not_matched() { // A type like `NukeAdjacentThing` must not match `\bNukeBuild\b`. const string input = "var x = new NukeBuilderXYZ();"; - var result = RewriteCsFilesStep.Rewrite(input); - result.EditCount.Should().Be(0); - result.Content.Should().Be(input); + (tempDirectory / "Build.cs").WriteAllText(input); + + await new RewriteCsFilesStep().ExecuteAsync(context, summary); + + summary.EditCount.Should().Be(0); + var buildCs = (tempDirectory / "Build.cs").ReadAllText().Trim(); + buildCs.Should().Be(input); } [Fact] - public void DoesNotMatchLowercaseNukePrefix() + public async Task Lowercase_nuke_prefix_in_a_path_is_not_matched() { // ".nuke/foo" filenames stay as-is — handled by ScriptRewriter / DirectoryRenamer. const string input = """var path = "/repo/.nuke/parameters.json";"""; - var result = RewriteCsFilesStep.Rewrite(input); - result.EditCount.Should().Be(0); + (tempDirectory / "Build.cs").WriteAllText(input); + + await new RewriteCsFilesStep().ExecuteAsync(context, summary); + + summary.EditCount.Should().Be(0); } [Fact] - public void RewritesNukeProjectModelUsingToSolutions() + public async Task Nuke_project_model_using_is_rewritten_to_solutions() { // The solution types moved to Fallout.Solutions in v11 — a NUKE-era // `using` must land there, not on the dead Fallout.Common.ProjectModel. - const string input = "using Nuke.Common.ProjectModel;"; - var result = RewriteCsFilesStep.Rewrite(input); - result.EditCount.Should().Be(1); - result.Content.Should().Be("using Fallout.Solutions;"); + (tempDirectory / "Build.cs").WriteAllText("using Nuke.Common.ProjectModel;"); + + await new RewriteCsFilesStep().ExecuteAsync(context, summary); + + summary.EditCount.Should().Be(1); + var buildCs = (tempDirectory / "Build.cs").ReadAllText().Trim(); + buildCs.Should().Be("using Fallout.Solutions;"); } [Fact] - public void RewritesQualifiedNukeProjectModelTypeToSolutions() + public async Task Qualified_nuke_project_model_type_is_rewritten_to_solutions() { - const string input = "Nuke.Common.ProjectModel.Solution x;"; - var result = RewriteCsFilesStep.Rewrite(input); - result.EditCount.Should().Be(1); - result.Content.Should().Be("Fallout.Solutions.Solution x;"); + (tempDirectory / "Build.cs").WriteAllText("Nuke.Common.ProjectModel.Solution x;"); + + await new RewriteCsFilesStep().ExecuteAsync(context, summary); + + summary.EditCount.Should().Be(1); + var buildCs = (tempDirectory / "Build.cs").ReadAllText().Trim(); + buildCs.Should().Be("Fallout.Solutions.Solution x;"); } [Fact] - public void RewritesAlreadyPartiallyMigratedProjectModelNamespace() + public async Task Already_partially_migrated_project_model_namespace_is_salvaged() { // Code previously run through a prefix-only migrator lands on the dead // Fallout.Common.ProjectModel; the ProjectModel rule salvages it. - const string input = "using Fallout.Common.ProjectModel;"; - var result = RewriteCsFilesStep.Rewrite(input); - result.EditCount.Should().Be(1); - result.Content.Should().Be("using Fallout.Solutions;"); + (tempDirectory / "Build.cs").WriteAllText("using Fallout.Common.ProjectModel;"); + + await new RewriteCsFilesStep().ExecuteAsync(context, summary); + + summary.EditCount.Should().Be(1); + var buildCs = (tempDirectory / "Build.cs").ReadAllText().Trim(); + buildCs.Should().Be("using Fallout.Solutions;"); } [Fact] - public void DoesNotMatchProjectModelAsPartOfAnotherIdentifier() + public async Task ProjectModel_as_part_of_another_identifier_is_not_truncated() { // `ProjectModelFoo` must not be truncated by the ProjectModel rule. - const string input = "using Nuke.Common.ProjectModelFoo;"; - var result = RewriteCsFilesStep.Rewrite(input); - result.EditCount.Should().Be(1); - result.Content.Should().Be("using Fallout.Common.ProjectModelFoo;"); + (tempDirectory / "Build.cs").WriteAllText("using Nuke.Common.ProjectModelFoo;"); + + await new RewriteCsFilesStep().ExecuteAsync(context, summary); + + summary.EditCount.Should().Be(1); + var buildCs = (tempDirectory / "Build.cs").ReadAllText().Trim(); + buildCs.Should().Be("using Fallout.Common.ProjectModelFoo;"); } } diff --git a/tests/Fallout.Migrate.Specs/RewriteCsprojsStepSpecs.cs b/tests/Fallout.Migrate.Specs/RewriteCsprojsStepSpecs.cs index d3628111..4686d5b4 100644 --- a/tests/Fallout.Migrate.Specs/RewriteCsprojsStepSpecs.cs +++ b/tests/Fallout.Migrate.Specs/RewriteCsprojsStepSpecs.cs @@ -1,74 +1,101 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Fallout.Common.IO; +using Fallout.Migrate.Common; +using Fallout.Migrate.Steps; using FluentAssertions; using Xunit; -using Fallout.Migrate.Steps; namespace Fallout.Migrate.Specs; -public class RewriteCsprojsStepSpecs +public class RewriteCsprojsStepSpecs : IDisposable { private const string TestFalloutVersion = "11.0.0"; - [Fact] - public void Nuke_package_references_are_renamed_to_their_fallout_equivalents() - { - const string input = """ - - - - - - - """; + private readonly AbsolutePath tempDirectory; + private readonly MigrationContext context; + private readonly Summary summary = new(); - var result = RewriteCsprojsStep.Rewrite(input, TestFalloutVersion); + public RewriteCsprojsStepSpecs() + { + tempDirectory = AbsolutePath.Temp("fallout-migrate-test"); + (tempDirectory / "build").CreateDirectory(); + context = new MigrationContext(tempDirectory, dryRun: false, TextWriter.Null) + { + FalloutVersion = TestFalloutVersion + }; + } - result.EditCount.Should().Be(2); - result.Content.Should().Contain(@"Include=""Fallout.Common"""); - result.Content.Should().Contain(@"Include=""Fallout.Components"""); - result.Content.Should().NotContain(@"Include=""Nuke."); + public void Dispose() + { + tempDirectory.DeleteDirectory(); } [Fact] - public void Nuke_root_directory_property_is_renamed() + public async Task Nuke_package_references_are_renamed_to_their_fallout_equivalents() { - const string input = """ - - - .\.. - - - """; - - var result = RewriteCsprojsStep.Rewrite(input, TestFalloutVersion); + (tempDirectory / "build" / "_build.csproj").WriteAllText(""" + + + + + + + """); + + await new RewriteCsprojsStep().ExecuteAsync(context, summary); + + summary.EditCount.Should().Be(2); + var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText(); + buildCsproj.Should().Contain(@"Include=""Fallout.Common"""); + buildCsproj.Should().Contain(@"Include=""Fallout.Components"""); + buildCsproj.Should().NotContain(@"Include=""Nuke."); + } - result.EditCount.Should().Be(2); // 1 opening + 1 closing tag - result.Content.Should().Contain(""); - result.Content.Should().Contain(""); - result.Content.Should().NotContain(""); + [Fact] + public async Task Nuke_root_directory_property_is_renamed() + { + (tempDirectory / "build" / "_build.csproj").WriteAllText(""" + + + .\.. + + + """); + + await new RewriteCsprojsStep().ExecuteAsync(context, summary); + + summary.EditCount.Should().Be(2); // 1 opening + 1 closing tag + var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText(); + buildCsproj.Should().Contain(""); + buildCsproj.Should().Contain(""); + buildCsproj.Should().NotContain(""); } [Fact] - public void Telemetry_version_property_is_stripped_instead_of_renamed() + public async Task Telemetry_version_property_is_stripped_instead_of_renamed() { // Telemetry was removed from Fallout (ADR-0010): NukeTelemetryVersion is dropped, not // renamed to a dead FalloutTelemetryVersion. Sibling properties are left intact. - const string input = """ - - - .\.. - 1 - - - """; - - var result = RewriteCsprojsStep.Rewrite(input, TestFalloutVersion); - - result.Content.Should().NotContain("TelemetryVersion"); - result.Content.Should().Contain(""); + (tempDirectory / "build" / "_build.csproj").WriteAllText(""" + + + .\.. + 1 + + + """); + + await new RewriteCsprojsStep().ExecuteAsync(context, summary); + + var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText(); + buildCsproj.Should().NotContain("TelemetryVersion"); + buildCsproj.Should().Contain(""); } [Fact] - public void Unrelated_nuke_prefixed_properties_are_left_alone() + public async Task Unrelated_nuke_prefixed_properties_are_left_alone() { const string input = """ @@ -77,15 +104,17 @@ public void Unrelated_nuke_prefixed_properties_are_left_alone() """; + (tempDirectory / "build" / "_build.csproj").WriteAllText(input, eofLineBreak: false); - var result = RewriteCsprojsStep.Rewrite(input, TestFalloutVersion); + await new RewriteCsprojsStep().ExecuteAsync(context, summary); - result.EditCount.Should().Be(0); - result.Content.Should().Be(input); + summary.EditCount.Should().Be(0); + var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText(); + buildCsproj.Should().Be(input); } [Fact] - public void Content_without_nuke_references_is_returned_unchanged() + public async Task Content_without_nuke_references_is_returned_unchanged() { const string input = """ @@ -94,95 +123,103 @@ public void Content_without_nuke_references_is_returned_unchanged() """; + (tempDirectory / "build" / "_build.csproj").WriteAllText(input, eofLineBreak: false); - var result = RewriteCsprojsStep.Rewrite(input, TestFalloutVersion); + await new RewriteCsprojsStep().ExecuteAsync(context, summary); - result.EditCount.Should().Be(0); - result.Content.Should().Be(input); + summary.EditCount.Should().Be(0); + var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText(); + buildCsproj.Should().Be(input); } [Fact] - public void Inline_nuke_package_versions_are_bumped_to_the_current_fallout_version() + public async Task Inline_nuke_package_versions_are_bumped_to_the_current_fallout_version() { // Regression guard for #217: NUKE-era pins like 10.1.0 never existed as Fallout.X // and would trip NU1603 on the migrated project. Migrate must bump in the same pass. - const string input = """ - - - - - - - """; - - var result = RewriteCsprojsStep.Rewrite(input, TestFalloutVersion); - - result.Content.Should().Contain(@"Include=""Fallout.Common"" Version=""11.0.0"""); - result.Content.Should().Contain(@"Include=""Fallout.Components"" Version=""11.0.0"""); - result.Content.Should().NotContain(@"Version=""10.1.0"""); + (tempDirectory / "build" / "_build.csproj").WriteAllText(""" + + + + + + + """); + + await new RewriteCsprojsStep().ExecuteAsync(context, summary); + + var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText(); + buildCsproj.Should().Contain(@"Include=""Fallout.Common"""); + buildCsproj.Should().Contain(@"Include=""Fallout.Components"""); + buildCsproj.Should().NotContain(@"Version=""10.1.0"""); + buildCsproj.Should().Contain($@"Version=""{TestFalloutVersion}"""); } [Fact] - public void Version_is_bumped_across_extra_attributes_between_include_and_version() + public async Task Version_is_bumped_across_extra_attributes_between_include_and_version() { // PrivateAssets / IncludeAssets are common NUKE-era attributes that sit between // Include and Version. The combined-rewrite pattern needs to tolerate them. - const string input = """ - - - - - - """; - - var result = RewriteCsprojsStep.Rewrite(input, TestFalloutVersion); - - result.Content.Should().Contain(@"Include=""Fallout.Common"" PrivateAssets=""all"" Version=""11.0.0"""); + (tempDirectory / "build" / "_build.csproj").WriteAllText(""" + + + + + + """); + + await new RewriteCsprojsStep().ExecuteAsync(context, summary); + + var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText(); + buildCsproj.Should().Contain(@"Include=""Fallout.Common"" PrivateAssets=""all"""); + buildCsproj.Should().NotContain(@"Version=""10.1.0"""); } [Fact] - public void Centrally_managed_references_do_not_gain_an_inline_version() + public async Task Centrally_managed_references_do_not_gain_an_inline_version() { // Central package management — no inline Version attribute, version lives in // Directory.Packages.props. The namespace-only pass still renames Nuke. → Fallout. // but we must NOT inject a Version where there wasn't one. - const string input = """ - - - - - - """; - - var result = RewriteCsprojsStep.Rewrite(input, TestFalloutVersion); - - result.Content.Should().Contain(@""); - result.Content.Should().NotContain(@"Version="); + (tempDirectory / "build" / "_build.csproj").WriteAllText(""" + + + + + + """); + + await new RewriteCsprojsStep().ExecuteAsync(context, summary); + + var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText(); + buildCsproj.Should().Contain(@""); + buildCsproj.Should().NotContain("Version="); } [Fact] - public void Conflicting_system_security_cryptography_xml_pin_is_stripped() + public async Task Conflicting_system_security_cryptography_xml_pin_is_stripped() { // #217: NUKE-era projects often carry an explicit System.Security.Cryptography.Xml pin // that conflicts with Fallout.Common's transitive >= 10.0.6 requirement (NU1605 downgrade). // Removing the explicit pin lets the transitive version win. - const string input = """ - - - - - - - """; - - var result = RewriteCsprojsStep.Rewrite(input, TestFalloutVersion); - - result.Content.Should().NotContain("System.Security.Cryptography.Xml"); - result.Content.Should().Contain(@"Include=""Fallout.Common"" Version=""11.0.0"""); + (tempDirectory / "build" / "_build.csproj").WriteAllText(""" + + + + + + + """); + + await new RewriteCsprojsStep().ExecuteAsync(context, summary); + + var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText(); + buildCsproj.Should().NotContain("System.Security.Cryptography.Xml"); + buildCsproj.Should().Contain(@"Include=""Fallout.Common"""); } [Fact] - public void Other_system_packages_are_left_alone() + public async Task Other_system_packages_are_left_alone() { // Only System.Security.Cryptography.Xml is the known culprit. Other System.* packages // (System.Text.Json etc.) stay as the user pinned them — they're not in any known @@ -195,10 +232,12 @@ public void Other_system_packages_are_left_alone() """; + (tempDirectory / "build" / "_build.csproj").WriteAllText(input, eofLineBreak: false); - var result = RewriteCsprojsStep.Rewrite(input, TestFalloutVersion); + await new RewriteCsprojsStep().ExecuteAsync(context, summary); - result.EditCount.Should().Be(0); - result.Content.Should().Be(input); + summary.EditCount.Should().Be(0); + var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText(); + buildCsproj.Should().Be(input); } } diff --git a/tests/Fallout.Migrate.Specs/ScriptRewriterSpecs.cs b/tests/Fallout.Migrate.Specs/ScriptRewriterSpecs.cs deleted file mode 100644 index 53a912ca..00000000 --- a/tests/Fallout.Migrate.Specs/ScriptRewriterSpecs.cs +++ /dev/null @@ -1,191 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Fallout.Migrate.Common; -using FluentAssertions; -using Xunit; -using Fallout.Migrate.Steps; - -namespace Fallout.Migrate.Specs; - -public class ScriptRewriterSpecs -{ - [Fact] - public void Dotnet_nuke_invocations_become_dotnet_fallout() - { - var result = ScriptRewriter.Rewrite("dotnet nuke Compile"); - result.EditCount.Should().Be(1); - result.Content.Should().Be("dotnet fallout Compile"); - } - - [Fact] - public void Dot_nuke_directory_references_become_dot_fallout() - { - var result = ScriptRewriter.Rewrite("""TEMP_DIRECTORY="$SCRIPT_DIR/.nuke/temp" """); - result.EditCount.Should().Be(1); - result.Content.Should().Contain(".fallout/temp"); - result.Content.Should().NotContain(".nuke/"); - } - - [Fact] - public void Legacy_nuke_env_vars_are_renamed_to_their_fallout_equivalents() - { - const string input = """ - $env:NUKE_GLOBAL_TOOL_VERSION = "10.0" - """; - - var result = ScriptRewriter.Rewrite(input); - result.EditCount.Should().Be(1); - result.Content.Should().Contain("FALLOUT_GLOBAL_TOOL_VERSION"); - } - - [Theory] - [InlineData("export NUKE_TELEMETRY_OPTOUT=1")] // build.sh - [InlineData("""$env:NUKE_TELEMETRY_OPTOUT = "1" """)] // build.ps1 - [InlineData("set NUKE_TELEMETRY_OPTOUT=1")] // build.cmd - public void Telemetry_opt_out_line_is_stripped_entirely(string optOutLine) - { - // Telemetry was removed from Fallout (ADR-0010) — the opt-out is dropped, not renamed - // to a dead FALLOUT_TELEMETRY_OPTOUT. Whichever bootstrap script spelled it, the whole - // line goes and the surrounding ones are untouched. - var input = $""" - export DOTNET_ROLL_FORWARD="Major" - {optOutLine} - dotnet nuke "$@" - """; - - var result = ScriptRewriter.Rewrite(input); - - result.Content.Should().NotContain("TELEMETRY_OPTOUT"); - result.Content.Should().Contain("DOTNET_ROLL_FORWARD"); - result.Content.Should().Contain("dotnet fallout"); - } - - [Fact] - public void Plain_word_nuke_in_prose_is_left_alone() - { - // The word "nuke" in a comment or string isn't a command invocation. - const string input = "# This was previously a NUKE-based build."; - var result = ScriptRewriter.Rewrite(input); - result.EditCount.Should().Be(0); - } - - [Fact] - public async Task Removes_Nuke_enterprise_unix_bootstrapper_leftovers() - { - var filename = "build.sh"; - var dir = CreateBootstrapScript(filename, """ - echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" - - if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "$NUKE_ENTERPRISE_TOKEN" != "" ]]; then - "$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true - "$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true - fi - - "$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet - "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" - """); - - Summary summary = await ExecuteMigrationStep(dir); - - var buildSh = await File.ReadAllTextAsync(Path.Combine(dir, filename)); - summary.EditCount.Should().Be(1); - buildSh.Should().Be( - """ - echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" - - "$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet - "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" - """); - } - - [Fact] - public async Task Does_not_throw_when_Nuke_enterprise_check_is_last_in_file() - { - var filename = "build.sh"; - var dir = CreateBootstrapScript(filename,""" - if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "$NUKE_ENTERPRISE_TOKEN" != "" ]]; then - "$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true - "$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true - fi - """); - - Summary summary = await ExecuteMigrationStep(dir); - - var buildSh = await File.ReadAllTextAsync(Path.Combine(dir, filename)); - summary.EditCount.Should().Be(1); - buildSh.Should().Be(""); - } - - [Fact] - public async Task Removes_Nuke_enterprise_windows_bootstrapper_leftovers() - { - var filename = "build.ps1"; - var dir = CreateBootstrapScript(filename,""" - Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" - - if (Test-Path env:NUKE_ENTERPRISE_TOKEN) { - & $env:DOTNET_EXE nuget remove source "nuke-enterprise" > $null - & $env:DOTNET_EXE nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password $env:NUKE_ENTERPRISE_TOKEN > $null - } - - ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } - ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } - """); - - Summary summary = await ExecuteMigrationStep(dir); - - var buildPs1 = await File.ReadAllTextAsync(Path.Combine(dir, filename)); - summary.EditCount.Should().Be(1); - buildPs1.Should().Be( - """ - Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" - - ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } - ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } - """); - } - - [Fact] - public async Task Leaves_bootstrapper_scripts_without_leftovers_alone() - { - var filename = "build.sh"; - var dir = CreateBootstrapScript(filename,""" - echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" - - "$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet - "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" - """); - - Summary summary = await ExecuteMigrationStep(dir); - - var buildSh = await File.ReadAllTextAsync(Path.Combine(dir, filename)); - summary.EditCount.Should().Be(0); - buildSh.Should().Be( - """ - echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" - - "$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet - "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" - """); - } - - private string CreateBootstrapScript(string bootstrapFile, string bootstrapFileConent) - { - var dir = Path.Combine(Path.GetTempPath(), "fallout-migrate-test-" + Guid.NewGuid().ToString("N")[..8]); - Directory.CreateDirectory(dir); - - File.WriteAllText(Path.Combine(dir, bootstrapFile), bootstrapFileConent); - - return dir; - } - - private static async Task ExecuteMigrationStep(string dir) - { - var step = new CleanupBootstrapScriptsStep(); - var summary = new Summary(); - await step.ExecuteAsync(new MigrationContext(dir, false, TextWriter.Null), summary); - return summary; - } - -}