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"));
+ }
}