From f7e98a4128e73de0f8ef1b80e0e9288cc714730b Mon Sep 17 00:00:00 2001 From: Adam Poit Date: Mon, 7 Oct 2024 11:52:17 -0700 Subject: [PATCH] Allow void entry point when calling BuildRunner.Execute. --- src/Faithlife.Build/BuildRunner.cs | 12 +- src/Faithlife.Build/BuildRunnerSettings.cs | 15 ++ .../Faithlife.Build.Tests/BuildRunnerTests.cs | 153 ++++++++++++++++++ 3 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 src/Faithlife.Build/BuildRunnerSettings.cs diff --git a/src/Faithlife.Build/BuildRunner.cs b/src/Faithlife.Build/BuildRunner.cs index c289d8d..9042c83 100644 --- a/src/Faithlife.Build/BuildRunner.cs +++ b/src/Faithlife.Build/BuildRunner.cs @@ -16,22 +16,26 @@ public static class BuildRunner /// /// The command-line arguments from Main. /// Called to initialize the build. + /// The build runner settings. /// The exit code for the build. - public static int Execute(string[] args, Action initialize) => - ExecuteAsync(args, initialize).GetAwaiter().GetResult(); + public static int Execute(string[] args, Action initialize, BuildRunnerSettings? settings = null) => + ExecuteAsync(args, initialize, settings).GetAwaiter().GetResult(); /// /// Executes an automated build. Called from Main. /// /// The command-line arguments from Main. /// Called to initialize the build. + /// The build runner settings. /// The exit code for the build. - public static async Task ExecuteAsync(string[] args, Action initialize) + public static async Task ExecuteAsync(string[] args, Action initialize, BuildRunnerSettings? settings = null) { + settings ??= new BuildRunnerSettings(); + ArgumentNullException.ThrowIfNull(args); ArgumentNullException.ThrowIfNull(initialize); - if (Assembly.GetEntryAssembly()?.EntryPoint?.ReturnType == typeof(void)) + if (!settings.AllowVoidEntrypoint && Assembly.GetEntryAssembly()?.EntryPoint?.ReturnType == typeof(void)) { Console.Error.WriteLine("Application entry point returns void; it should return the result of BuildRunner.Execute."); return 2; diff --git a/src/Faithlife.Build/BuildRunnerSettings.cs b/src/Faithlife.Build/BuildRunnerSettings.cs new file mode 100644 index 0000000..567f337 --- /dev/null +++ b/src/Faithlife.Build/BuildRunnerSettings.cs @@ -0,0 +1,15 @@ +namespace Faithlife.Build; + +/// +/// Settings for the BuildRunner; see . +/// +public sealed class BuildRunnerSettings +{ + /// + /// Allows the consumer's entry point to return void. + /// + /// For most consumers a void return entry point would be a mistake, as generally we should return + /// the result of BuildRunner.Execute. However, there are instances, such as unit testing, where we should allow + /// for a void return entry point. + public bool AllowVoidEntrypoint { get; set; } +} diff --git a/tests/Faithlife.Build.Tests/BuildRunnerTests.cs b/tests/Faithlife.Build.Tests/BuildRunnerTests.cs index 7e2df1b..b5f741e 100644 --- a/tests/Faithlife.Build.Tests/BuildRunnerTests.cs +++ b/tests/Faithlife.Build.Tests/BuildRunnerTests.cs @@ -15,4 +15,157 @@ public void NullInitializeThrows() { Assert.Throws(() => BuildRunner.Execute([], null!)); } + + [Test] + public void FailsOnMissingTarget() + { + using var error = new StringWriter(); + Console.SetError(error); + + var targetName = "target"; + + Assert.That(BuildRunner.Execute([targetName], build => { }, new BuildRunnerSettings { AllowVoidEntrypoint = true }), Is.EqualTo(2)); + Assert.That(error.ToString(), Does.Contain($"Target not found: {targetName}")); + } + + [Test] + public void PrintsDefaultTargets() + { + using var output = new StringWriter(); + Console.SetOut(output); + + Assert.That(BuildRunner.Execute([], build => { }, new BuildRunnerSettings { AllowVoidEntrypoint = true }), Is.EqualTo(0)); + + // Check for some known default targets + var outputString = output.ToString(); + Assert.That(outputString, Does.Contain("-n|--dry-run")); + Assert.That(outputString, Does.Contain("-s|--skip-dependencies")); + Assert.That(outputString, Does.Contain("-?|-h|--help")); + } + + [Test] + public void PrintsCustomTargets() + { + using var output = new StringWriter(); + Console.SetOut(output); + + var targetName = "target"; + var targetDescription = "This is a basic target."; + + Assert.That(BuildRunner.Execute([], build => + { + build.Target(targetName) + .Describe(targetDescription); + }, new BuildRunnerSettings { AllowVoidEntrypoint = true }), Is.EqualTo(0)); + Assert.That(output.ToString(), Does.Match($"{targetName}\\s+{targetDescription}")); + } + + [Test] + public void FailsOnFailedTarget() + { + using var output = new StringWriter(); + Console.SetOut(output); + + var targetName = "target"; + + Assert.That(BuildRunner.Execute(["--no-color", targetName], build => + { + build.Target(targetName) + .Does(() => + { + throw new InvalidOperationException(); + }); + }, new BuildRunnerSettings { AllowVoidEntrypoint = true }), Is.EqualTo(1)); + Assert.That(output.ToString(), Does.Contain($"{targetName}: FAILED!")); + } + + [Test] + public void DryRunSkipsTargetExecution() + { + using var output = new StringWriter(); + Console.SetOut(output); + + var targetName = "target"; + + Assert.That(BuildRunner.Execute(["--no-color", "--dry-run", targetName], build => + { + build.Target(targetName) + .Does(() => + { + throw new InvalidOperationException(); + }); + }, new BuildRunnerSettings { AllowVoidEntrypoint = true }), Is.EqualTo(0)); + Assert.That(output.ToString(), Does.Contain($"Succeeded ({targetName}) (dry run)")); + } + + [Test] + public void ExecutesDependencies() + { + using var output = new StringWriter(); + Console.SetOut(output); + + var firstTarget = "firstTarget"; + var secondTarget = "secondTarget"; + + Assert.That(BuildRunner.Execute(["--no-color", secondTarget], build => + { + build.Target(firstTarget); + + build.Target(secondTarget) + .DependsOn(firstTarget); + }, new BuildRunnerSettings { AllowVoidEntrypoint = true }), Is.EqualTo(0)); + + var outputString = output.ToString(); + Assert.That(outputString, Does.Contain($"{firstTarget}: Succeeded")); + Assert.That(outputString, Does.Contain($"{secondTarget}: Succeeded")); + } + + [Test] + public void SkipsDependencies() + { + using var output = new StringWriter(); + Console.SetOut(output); + + var firstTarget = "firstTarget"; + var secondTarget = "secondTarget"; + + Assert.That(BuildRunner.Execute(["--no-color", "--skip-dependencies", secondTarget], build => + { + build.Target(firstTarget); + + build.Target(secondTarget) + .DependsOn(firstTarget); + }, new BuildRunnerSettings { AllowVoidEntrypoint = true }), Is.EqualTo(0)); + + var outputString = output.ToString(); + Assert.That(outputString, Does.Not.Contain($"{firstTarget}: Succeeded")); + Assert.That(outputString, Does.Contain($"{secondTarget}: Succeeded")); + } + + [Test] + public void SkipsSpecificDependencies() + { + using var output = new StringWriter(); + Console.SetOut(output); + + var firstTarget = "firstTarget"; + var secondTarget = "secondTarget"; + var thirdTarget = "thirdTarget"; + + Assert.That(BuildRunner.Execute(["--no-color", "--skip", secondTarget, thirdTarget], build => + { + build.Target(firstTarget); + + build.Target(secondTarget); + + build.Target(thirdTarget) + .DependsOn(firstTarget) + .DependsOn(secondTarget); + }, new BuildRunnerSettings { AllowVoidEntrypoint = true }), Is.EqualTo(0)); + + var outputString = output.ToString(); + Assert.That(outputString, Does.Contain($"{firstTarget}: Succeeded")); + Assert.That(outputString, Does.Not.Contain($"{secondTarget}: Succeeded")); + Assert.That(outputString, Does.Contain($"{thirdTarget}: Succeeded")); + } }