Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions Optimum.Bootstrap.Core.Tests/BuildLayerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,36 @@ public async Task AnUnspawnableStepExecutableFailsTheBuildInsteadOfHangingTheWiz
}
}
}

public class ScriptBuildDriverWindowsCommandTests
{
[Fact]
public void BootstrapCommandUsesAnAbsoluteScriptPathFromTheSourceRoot()
{
var probe = new FakeSystemProbe { Os = OsKind.Windows };
string repo = Path.Combine(Path.GetTempPath(), "optimum-source");
BuildRequest request = new(repo, Path.Combine(Path.GetTempPath(), "optimum-output"));

var command = new ScriptBuildDriver(probe).BootstrapCommand(request);

Assert.Equal("-File", command.Args[0]);
Assert.Equal(Path.GetFullPath(Path.Combine(repo, "scripts", "bootstrap.ps1")), command.Args[1]);
Assert.True(Path.IsPathFullyQualified(command.Args[1]));
}

[Fact]
public void PackageCommandUsesAnAbsoluteScriptPathFromTheSourceRoot()
{
var probe = new FakeSystemProbe { Os = OsKind.Windows };
string repo = Path.Combine(Path.GetTempPath(), "optimum-source");
string output = Path.Combine(Path.GetTempPath(), "optimum-output");
BuildRequest request = new(repo, output);

var command = new ScriptBuildDriver(probe).PackageCommand(request);

Assert.Equal("-File", command.Args[0]);
Assert.Equal(Path.GetFullPath(Path.Combine(repo, "scripts", "package.ps1")), command.Args[1]);
Assert.True(Path.IsPathFullyQualified(command.Args[1]));
Assert.Equal(output, command.Args[3]);
}
}
109 changes: 109 additions & 0 deletions Optimum.Bootstrap.Core.Tests/SourceAcquisitionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,46 @@ namespace Optimum.Bootstrap.Core.Tests;

public class SourceCacheTests
{
[Fact]
public void WindowsCacheWithoutScriptsIsNotUsable()
{
var probe = new FakeSystemProbe { Os = OsKind.Windows };
probe.AddFile("/cache/forks.json");

Assert.False(SourceCache.IsUsableCheckout(probe, "/cache"));
}

[Fact]
public void WindowsCacheWithOnlyUnixBootstrapScriptIsNotUsable()
{
var probe = new FakeSystemProbe { Os = OsKind.Windows };
probe.AddFile("/cache/forks.json");
probe.AddFile("/cache/scripts/bootstrap.sh");

Assert.False(SourceCache.IsUsableCheckout(probe, "/cache"));
}

[Fact]
public void WindowsCacheMissingPackagingScriptIsNotUsable()
{
var probe = new FakeSystemProbe { Os = OsKind.Windows };
probe.AddFile("/cache/forks.json");
probe.AddFile("/cache/scripts/bootstrap.ps1");

Assert.False(SourceCache.IsUsableCheckout(probe, "/cache"));
}

[Fact]
public void WindowsCacheWithBothRequiredScriptsIsUsable()
{
var probe = new FakeSystemProbe { Os = OsKind.Windows };
probe.AddFile("/cache/forks.json");
probe.AddFile("/cache/scripts/bootstrap.ps1");
probe.AddFile("/cache/scripts/package.ps1");

Assert.True(SourceCache.IsUsableCheckout(probe, "/cache"));
}

[Theory]
[InlineData("0.3.14", "v0.3.14")]
[InlineData("1.0.0", "v1.0.0")]
Expand Down Expand Up @@ -75,6 +115,40 @@ public void CloneArgumentsOmitTheBranchWhenThereIsNoTag()
}
}

public class RepoRootTests
{
[Fact]
public void WindowsDiscoveryRejectsAUnixOnlyCheckout()
{
var probe = new FakeSystemProbe { Os = OsKind.Windows };
probe.AddFile("/repo/forks.json");
probe.AddFile("/repo/scripts/bootstrap.sh");

Assert.Null(RepoRoot.Discover(probe, "/repo"));
}

[Fact]
public void WindowsDiscoveryReturnsTheCheckoutWithBothPowerShellScripts()
{
var probe = new FakeSystemProbe { Os = OsKind.Windows };
probe.AddFile("/repo/forks.json");
probe.AddFile("/repo/scripts/bootstrap.ps1");
probe.AddFile("/repo/scripts/package.ps1");

Assert.Equal("/repo", RepoRoot.Discover(probe, "/repo"));
}

[Fact]
public void UnixDiscoveryKeepsItsExistingBootstrapShellContract()
{
var probe = new FakeSystemProbe { Os = OsKind.Linux };
probe.AddFile("/repo/forks.json");
probe.AddFile("/repo/scripts/bootstrap.sh");

Assert.Equal("/repo", RepoRoot.Discover(probe, "/repo"));
}
}

public class GitSourceProviderTests
{
[Fact]
Expand Down Expand Up @@ -118,6 +192,41 @@ public async Task ReusesACachedCheckoutWithoutTouchingGit()
Assert.Equal(cached, result.RepoRoot);
}

[Fact]
public async Task DoesNotReuseAnIncompleteWindowsCheckout()
{
var probe = new FakeSystemProbe { Os = OsKind.Windows };
probe.Environment["LOCALAPPDATA"] = "/cache";
string cached = "/cache/optimum/src-v0.3.14";
probe.AddFile($"{cached}/forks.json");
probe.AddFile($"{cached}/scripts/bootstrap.sh");
// No Git on PATH: an incomplete cache must be rejected, not reused.

var result = await new GitSourceProvider(probe)
.EnsureAsync(new SourceRequest("0.3.14"), NullBuildObserver.Instance, CancellationToken.None);

Assert.False(result.Ok);
Assert.Equal(FailureReason.SourceUnavailable, result.Reason);
}

[Fact]
public async Task ReusesACompleteWindowsCheckoutFromTheSourceCache()
{
var probe = new FakeSystemProbe { Os = OsKind.Windows };
probe.Environment["LOCALAPPDATA"] = "/cache";
string cached = "/cache/optimum/src-v0.3.14";
probe.AddFile($"{cached}/forks.json");
probe.AddFile($"{cached}/scripts/bootstrap.ps1");
probe.AddFile($"{cached}/scripts/package.ps1");
// No Git on PATH: proves the complete cached source is the one used.

var result = await new GitSourceProvider(probe)
.EnsureAsync(new SourceRequest("0.3.14"), NullBuildObserver.Instance, CancellationToken.None);

Assert.True(result.Ok);
Assert.Equal(cached, result.RepoRoot);
}

[Fact]
public async Task FailsWithSourceUnavailableWhenGitIsMissing()
{
Expand Down
9 changes: 4 additions & 5 deletions Optimum.Bootstrap.Core/Build/RepoRoot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ namespace Optimum.Bootstrap.Core.Build;

/// <summary>
/// Finds the Optimum checkout the engine has to drive: the nearest directory at
/// or above a starting point that holds <c>forks.json</c> next to
/// <c>scripts/bootstrap.sh</c>. Both front ends need this because the build
/// pipeline is still the shell scripts (INSTALLER-PLAN.md section 2).
/// or above a starting point that holds the manifest and the scripts required
/// by the probed platform. Both front ends need this because the build pipeline
/// is still the platform scripts (INSTALLER-PLAN.md section 2).
/// </summary>
public static class RepoRoot
{
Expand All @@ -18,8 +18,7 @@ public static class RepoRoot

for (string? dir = start; dir is not null; dir = Path.GetDirectoryName(dir))
{
if (probe.FileExists(Path.Combine(dir, "forks.json"))
&& probe.FileExists(Path.Combine(dir, "scripts", "bootstrap.sh")))
if (SourceCache.IsUsableCheckout(probe, dir))
return dir;
}

Expand Down
11 changes: 7 additions & 4 deletions Optimum.Bootstrap.Core/Build/ScriptBuildDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,11 @@ public async Task<BuildResult> RunAsync(
}
}

private (string Exe, IReadOnlyList<string> Args) BootstrapCommand(BuildRequest request)
internal (string Exe, IReadOnlyList<string> Args) BootstrapCommand(BuildRequest request)
{
if (probe.Os == OsKind.Windows)
{
List<string> win = ["-File", "scripts/bootstrap.ps1"];
List<string> win = ["-File", ScriptPath(request.RepoRoot, "bootstrap.ps1")];
if (request.ClientArchive is not null)
win.AddRange(["-ClientArchive", request.ClientArchive]);
if (request.Version is not null)
Expand All @@ -131,13 +131,13 @@ public async Task<BuildResult> RunAsync(
return ("bash", unix);
}

private (string Exe, IReadOnlyList<string> Args) PackageCommand(BuildRequest request)
internal (string Exe, IReadOnlyList<string> Args) PackageCommand(BuildRequest request)
{
string output = request.OutputDirectory;
switch (probe.Os)
{
case OsKind.Windows:
List<string> win = ["-File", "scripts/package.ps1", "-OutputDir", output];
List<string> win = ["-File", ScriptPath(request.RepoRoot, "package.ps1"), "-OutputDir", output];
if (request.ClientArchive is not null) win.AddRange(["-ClientArchive", request.ClientArchive]);
return (PwshExecutable(), win);
case OsKind.MacOs:
Expand All @@ -154,6 +154,9 @@ public async Task<BuildResult> RunAsync(
}
}

private static string ScriptPath(string repoRoot, string scriptName) =>
Path.GetFullPath(Path.Combine(repoRoot, "scripts", scriptName));

private string DotnetExecutable() => DotnetSdkProbe.Find(probe) ?? "dotnet";

/// <summary>
Expand Down
18 changes: 14 additions & 4 deletions Optimum.Bootstrap.Core/Build/SourceAcquisition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,20 @@ internal static string SanitizeVersion(string version)
return v.Length > 1 && v[0] == 'v' && char.IsDigit(v[1]) ? v : null;
}

/// <summary>True when a directory holds the two files the pipeline needs.</summary>
public static bool IsUsableCheckout(ISystemProbe probe, string directory) =>
probe.FileExists(Path.Combine(directory, "forks.json"))
&& probe.FileExists(Path.Combine(directory, "scripts", "bootstrap.sh"));
/// <summary>
/// True when a directory holds the manifest and the scripts required by the
/// platform's build and packaging pipeline.
/// </summary>
public static bool IsUsableCheckout(ISystemProbe probe, string directory)
{
if (!probe.FileExists(Path.Combine(directory, "forks.json")))
return false;

return probe.Os == OsKind.Windows
? probe.FileExists(Path.Combine(directory, "scripts", "bootstrap.ps1"))
&& probe.FileExists(Path.Combine(directory, "scripts", "package.ps1"))
: probe.FileExists(Path.Combine(directory, "scripts", "bootstrap.sh"));
}

internal static IReadOnlyList<string> CloneArguments(string? tagRef, string targetDirectory)
{
Expand Down