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
59 changes: 52 additions & 7 deletions tests/OpenCode.Sdk.Tests/Harness/PinnedServerPrerequisites.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO.Abstractions;
Expand All @@ -10,9 +11,10 @@ namespace OpenCode.Sdk.Tests.Harness;

/// <summary>
/// What every source-run server in this assembly needs before its first test: the pinned submodule
/// checked out, its dependencies installed, and on PATH the Bun the pin's <c>packageManager</c>
/// names. It is checked once, before discovery, because TUnit reports a failed discovery hook as one
/// error for the module, where a failed session hook fails every test at 0 ms with the same message.
/// checked out at this repository's recorded commit, its dependencies installed, and on PATH the Bun
/// the pin's <c>packageManager</c> names. It is checked once, before discovery, because TUnit reports
/// a failed discovery hook as one error for the module, where a failed session hook fails every test
/// at 0 ms with the same message.
/// A missing piece refuses the whole module, a run filtered to tests that start no server included:
/// every gate and CI leg prepares the checkout, so an unprepared one is a mistake to name before it
/// costs a run, not a mode to support. One exception is explicit: <see cref="BunMismatchVariable"/>
Expand Down Expand Up @@ -61,6 +63,8 @@ public static async Task Report_An_Allowed_Bun_Mismatch_Async(CancellationToken
/// <param name="repositoryRoot">This repository's root.</param>
/// <param name="bunVersion">What <c>bun --version</c> printed, or null when no Bun is on PATH.</param>
/// <param name="bunMismatch">The value of <see cref="BunMismatchVariable"/>, or null when it is unset.</param>
/// <param name="recordedCommit">The submodule's gitlink in this repository's index, or null when unreadable.</param>
/// <param name="checkedOutCommit">The submodule's checked-out HEAD, or null when unreadable.</param>
/// <param name="cancellationToken">The caller's token.</param>
/// <returns>
/// The problems in the order to fix them, empty when the checkout is prepared, and the Bun
Expand All @@ -71,6 +75,8 @@ internal static async Task<Readiness> FindAsync(
string repositoryRoot,
string? bunVersion,
string? bunMismatch,
string? recordedCommit,
string? checkedOutCommit,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(fileSystem);
Expand All @@ -93,6 +99,20 @@ internal static async Task<Readiness> FindAsync(
return new Readiness(problems, AllowedMismatch: null);
}

foreach (var (commit, name) in new[] { (recordedCommit, "recorded gitlink"), (checkedOutCommit, "checked-out HEAD") })
{
if (string.IsNullOrWhiteSpace(commit))
{
problems.Add($"Cannot read the pin's {name} at '{submodule}'. Run: git submodule update --init --depth 1 external/opencode");
}
}

if (!string.IsNullOrWhiteSpace(recordedCommit) && !string.IsNullOrWhiteSpace(checkedOutCommit)
&& !string.Equals(recordedCommit, checkedOutCommit, StringComparison.Ordinal))
{
problems.Add($"The pin's checkout at '{submodule}' is {checkedOutCommit}, but this repository records {recordedCommit}. Run: git submodule update --init --depth 1 external/opencode");
}

string? allowedMismatch = null;
var pinned = await PinnedBunVersionAsync(fileSystem, submodule, cancellationToken).ConfigureAwait(false);
if (pinned.Problem is { } unpinned)
Expand Down Expand Up @@ -153,11 +173,28 @@ internal static string Describe(string module, string repositoryRoot, IReadOnlyL
var fileSystem = new RealFileSystem();
var repositoryRoot = new PinnedServerCommand(fileSystem).RepositoryRoot;
var bunVersion = await ReadBunVersionAsync(cancellationToken).ConfigureAwait(false);
var submodule = Submodule(fileSystem, repositoryRoot);
var commits = new List<string?>();
foreach (var arguments in new[] { $"-C \"{repositoryRoot}\" ls-files -s -- external/opencode", $"-C \"{submodule}\" rev-parse HEAD" })
{
try
{
commits.Add(await ReadProcessOutputAsync("git", arguments, "git " + arguments, cancellationToken).ConfigureAwait(false));
}
catch (Exception exception) when (exception is InvalidOperationException or Win32Exception)
{
commits.Add(null);
}
}

var gitlink = commits[0]?.Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries);
var readiness = await FindAsync(
fileSystem,
repositoryRoot,
bunVersion,
Environment.GetEnvironmentVariable(BunMismatchVariable),
gitlink is { Length: 4 } && gitlink[0] == "160000" && gitlink[2] == "0" ? gitlink[1] : null,
commits[1],
cancellationToken).ConfigureAwait(false);
return (repositoryRoot, readiness);
}
Expand Down Expand Up @@ -198,12 +235,20 @@ private static async Task<PinnedBun> PinnedBunVersionAsync(IFileSystem fileSyste
return null;
}

return await ReadProcessOutputAsync(
bun.IsBatchScript ? "cmd.exe" : bun.Path,
bun.IsBatchScript ? "/d /c \"\"" + bun.Path + "\" --version\"" : "--version",
bun.Path + " --version", cancellationToken).ConfigureAwait(false);
}

private static async Task<string> ReadProcessOutputAsync(string fileName, string arguments, string command, CancellationToken cancellationToken)
{
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = bun.IsBatchScript ? "cmd.exe" : bun.Path,
Arguments = bun.IsBatchScript ? "/d /c \"\"" + bun.Path + "\" --version\"" : "--version",
FileName = fileName,
Arguments = arguments,
CreateNoWindow = true,
RedirectStandardError = true,
RedirectStandardOutput = true,
Expand All @@ -213,7 +258,7 @@ private static async Task<PinnedBun> PinnedBunVersionAsync(IFileSystem fileSyste

if (!process.Start())
{
throw new InvalidOperationException($"'{bun.Path} --version' did not start.");
throw new InvalidOperationException($"'{command}' did not start.");
}

// The readers drain on their own token: cancelling them first could fill a pipe the child
Expand All @@ -236,7 +281,7 @@ private static async Task<PinnedBun> PinnedBunVersionAsync(IFileSystem fileSyste
return process.ExitCode is 0
? output.Trim()
: throw new InvalidOperationException(
$"'{bun.Path} --version' exited with code {process.ExitCode.ToString(CultureInfo.InvariantCulture)}: {error.Trim()}");
$"'{command}' exited with code {process.ExitCode.ToString(CultureInfo.InvariantCulture)}: {error.Trim()}");
}

/// <summary>What the pinned server still needs, and the Bun mismatch the opt-in allowed.</summary>
Expand Down
101 changes: 85 additions & 16 deletions tests/OpenCode.Sdk.Tests/Harness/PinnedServerPrerequisitesTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Testably.Abstractions.Testing;
using Testably.Abstractions.Testing.Initializer;
using TUnit.Assertions.Enums;

namespace OpenCode.Sdk.Tests.Harness;

Expand All @@ -12,6 +13,8 @@ public sealed class PinnedServerPrerequisitesTests
{
private const string PinnedBun = "1.4.2";
private const string OtherBun = "1.4.3";
private const string RecordedCommit = "1111111111111111111111111111111111111111";
private const string OtherCommit = "2222222222222222222222222222222222222222";
private const string PinnedPackageJson = "{\"packageManager\":\"bun@" + PinnedBun + "\"}";

[Test]
Expand All @@ -20,7 +23,7 @@ public async Task FindAsync_Should_Name_Only_The_Submodule_When_The_Pinned_Sourc
var fileSystem = new MockFileSystem();
var root = Root(fileSystem);

var readiness = await PinnedServerPrerequisites.FindAsync(fileSystem, root, PinnedBun, bunMismatch: null, CancellationToken.None);
var readiness = await FindWithMatchingCommitsAsync(fileSystem, root, PinnedBun, bunMismatch: null);

await Assert.That(readiness.Problems).HasSingleItem();
await Assert.That(readiness.Problems[0]).Contains("git submodule update --init --depth 1 external/opencode");
Expand All @@ -33,7 +36,7 @@ public async Task FindAsync_Should_Name_The_Install_When_The_Dependencies_Are_Mi
var root = Root(fileSystem);
CheckOut(fileSystem, root, PinnedPackageJson);

var readiness = await PinnedServerPrerequisites.FindAsync(fileSystem, root, PinnedBun, bunMismatch: null, CancellationToken.None);
var readiness = await FindWithMatchingCommitsAsync(fileSystem, root, PinnedBun, bunMismatch: null);

await Assert.That(readiness.Problems).HasSingleItem();
await Assert.That(readiness.Problems[0]).Contains("bun install --frozen-lockfile --ignore-scripts");
Expand All @@ -45,7 +48,7 @@ public async Task FindAsync_Should_Name_Both_Versions_When_The_Bun_On_Path_Is_No
var fileSystem = new MockFileSystem();
var root = Prepared(fileSystem);

var readiness = await PinnedServerPrerequisites.FindAsync(fileSystem, root, OtherBun, bunMismatch: null, CancellationToken.None);
var readiness = await FindWithMatchingCommitsAsync(fileSystem, root, OtherBun, bunMismatch: null);

await Assert.That(readiness.Problems).HasSingleItem();
await Assert.That(readiness.Problems[0]).Contains("The bun on PATH is " + OtherBun);
Expand All @@ -60,7 +63,7 @@ public async Task FindAsync_Should_Name_The_Pinned_Bun_When_None_Is_On_Path()
var fileSystem = new MockFileSystem();
var root = Prepared(fileSystem);

var readiness = await PinnedServerPrerequisites.FindAsync(fileSystem, root, bunVersion: null, bunMismatch: null, CancellationToken.None);
var readiness = await FindWithMatchingCommitsAsync(fileSystem, root, bunVersion: null, bunMismatch: null);

await Assert.That(readiness.Problems).HasSingleItem();
await Assert.That(readiness.Problems[0]).Contains("No bun is on PATH. Install bun " + PinnedBun);
Expand All @@ -74,7 +77,7 @@ public async Task FindAsync_Should_Refuse_A_Pin_That_Names_No_Bun()
CheckOut(fileSystem, root, "{\"packageManager\":\"pnpm@9.0.0\"}");
Install(fileSystem, root);

var readiness = await PinnedServerPrerequisites.FindAsync(fileSystem, root, PinnedBun, bunMismatch: null, CancellationToken.None);
var readiness = await FindWithMatchingCommitsAsync(fileSystem, root, PinnedBun, bunMismatch: null);

await Assert.That(readiness.Problems).HasSingleItem();
await Assert.That(readiness.Problems[0]).Contains("names no bun@ packageManager");
Expand All @@ -86,7 +89,7 @@ public async Task FindAsync_Should_Pass_A_Prepared_Checkout_With_The_Pinned_Bun(
var fileSystem = new MockFileSystem();
var root = Prepared(fileSystem);

var readiness = await PinnedServerPrerequisites.FindAsync(fileSystem, root, PinnedBun, bunMismatch: null, CancellationToken.None);
var readiness = await FindWithMatchingCommitsAsync(fileSystem, root, PinnedBun, bunMismatch: null);

await Assert.That(readiness.Problems).IsEmpty();
await Assert.That(readiness.AllowedMismatch).IsNull();
Expand All @@ -98,8 +101,8 @@ public async Task FindAsync_Should_Allow_And_Report_Another_Bun_When_The_Mismatc
var fileSystem = new MockFileSystem();
var root = Prepared(fileSystem);

var readiness = await PinnedServerPrerequisites.FindAsync(
fileSystem, root, OtherBun, PinnedServerPrerequisites.AllowBunMismatch, CancellationToken.None);
var readiness = await FindWithMatchingCommitsAsync(
fileSystem, root, OtherBun, PinnedServerPrerequisites.AllowBunMismatch);

await Assert.That(readiness.Problems).IsEmpty();
await Assert.That(readiness.AllowedMismatch)
Expand All @@ -113,8 +116,8 @@ public async Task FindAsync_Should_Refuse_No_Bun_On_Path_When_The_Mismatch_Is_Al
var fileSystem = new MockFileSystem();
var root = Prepared(fileSystem);

var readiness = await PinnedServerPrerequisites.FindAsync(
fileSystem, root, bunVersion: null, PinnedServerPrerequisites.AllowBunMismatch, CancellationToken.None);
var readiness = await FindWithMatchingCommitsAsync(
fileSystem, root, bunVersion: null, PinnedServerPrerequisites.AllowBunMismatch);

await Assert.That(readiness.Problems).HasSingleItem();
await Assert.That(readiness.Problems[0]).Contains("No bun is on PATH. Install bun " + PinnedBun);
Expand All @@ -128,8 +131,8 @@ public async Task FindAsync_Should_Still_Name_The_Install_When_The_Mismatch_Is_A
var root = Root(fileSystem);
CheckOut(fileSystem, root, PinnedPackageJson);

var readiness = await PinnedServerPrerequisites.FindAsync(
fileSystem, root, OtherBun, PinnedServerPrerequisites.AllowBunMismatch, CancellationToken.None);
var readiness = await FindWithMatchingCommitsAsync(
fileSystem, root, OtherBun, PinnedServerPrerequisites.AllowBunMismatch);

await Assert.That(readiness.Problems).HasSingleItem();
await Assert.That(readiness.Problems[0]).Contains("bun install --frozen-lockfile --ignore-scripts");
Expand All @@ -141,8 +144,8 @@ public async Task FindAsync_Should_Report_Nothing_When_The_Mismatch_Is_Allowed_B
var fileSystem = new MockFileSystem();
var root = Prepared(fileSystem);

var readiness = await PinnedServerPrerequisites.FindAsync(
fileSystem, root, PinnedBun, PinnedServerPrerequisites.AllowBunMismatch, CancellationToken.None);
var readiness = await FindWithMatchingCommitsAsync(
fileSystem, root, PinnedBun, PinnedServerPrerequisites.AllowBunMismatch);

await Assert.That(readiness.Problems).IsEmpty();
await Assert.That(readiness.AllowedMismatch).IsNull();
Expand All @@ -161,7 +164,7 @@ public async Task FindAsync_Should_Refuse_Any_Other_Value_Of_The_Mismatch_Variab
var fileSystem = new MockFileSystem();
var root = Prepared(fileSystem);

var readiness = await PinnedServerPrerequisites.FindAsync(fileSystem, root, PinnedBun, value, CancellationToken.None);
var readiness = await FindWithMatchingCommitsAsync(fileSystem, root, PinnedBun, value);

await Assert.That(readiness.Problems).HasSingleItem();
await Assert.That(readiness.Problems[0])
Expand All @@ -174,7 +177,7 @@ public async Task FindAsync_Should_Refuse_Another_Bun_When_The_Mismatch_Variable
var fileSystem = new MockFileSystem();
var root = Prepared(fileSystem);

var readiness = await PinnedServerPrerequisites.FindAsync(fileSystem, root, OtherBun, "yes", CancellationToken.None);
var readiness = await FindWithMatchingCommitsAsync(fileSystem, root, OtherBun, "yes");

await Assert.That(readiness.Problems).Count().IsEqualTo(2);
await Assert.That(readiness.Problems[0]).Contains("the only value it takes is 'allow'");
Expand All @@ -192,6 +195,72 @@ public async Task Describe_Should_Name_The_Module_And_List_Every_Problem()
await Assert.That(message).EndsWith(Environment.NewLine + " - first" + Environment.NewLine + " - second");
}

[Test]
public async Task FindAsync_Should_Name_A_Wrong_Checkout_Before_A_Missing_Install()
{
var fileSystem = new MockFileSystem();
var root = Root(fileSystem);
CheckOut(fileSystem, root, PinnedPackageJson);
var submodule = fileSystem.Path.Combine(root, "external", "opencode");

var readiness = await PinnedServerPrerequisites.FindAsync(
fileSystem, root, PinnedBun, bunMismatch: null, RecordedCommit, OtherCommit, CancellationToken.None);

await Assert.That(readiness.Problems).IsEquivalentTo([
$"The pin's checkout at '{submodule}' is 2222222222222222222222222222222222222222, but this repository records 1111111111111111111111111111111111111111. Run: git submodule update --init --depth 1 external/opencode",
$"The pinned server dependencies are not installed under '{submodule}'. Run there: bun install --frozen-lockfile --ignore-scripts",
], CollectionOrdering.Matching);
}

[Test]
[Arguments(null, RecordedCommit, "recorded gitlink")]
[Arguments(RecordedCommit, null, "checked-out HEAD")]
public async Task FindAsync_Should_Refuse_An_Unreadable_Commit(string? recorded, string? checkedOut, string unreadable)
{
var fileSystem = new MockFileSystem();
var root = Prepared(fileSystem);
var submodule = fileSystem.Path.Combine(root, "external", "opencode");

var readiness = await PinnedServerPrerequisites.FindAsync(
fileSystem, root, PinnedBun, bunMismatch: null, recorded, checkedOut, CancellationToken.None);

await Assert.That(readiness.Problems).IsEquivalentTo(
[$"Cannot read the pin's {unreadable} at '{submodule}'. Run: git submodule update --init --depth 1 external/opencode"], CollectionOrdering.Matching);
}

[Test]
public async Task FindAsync_Should_Not_Add_Commit_Problems_When_Source_Is_Missing()
{
var fileSystem = new MockFileSystem();
var root = Root(fileSystem);
var entry = fileSystem.Path.Combine(root, "external", "opencode", "packages", "cli", "src", "index.ts");

var readiness = await PinnedServerPrerequisites.FindAsync(
fileSystem, root, PinnedBun, bunMismatch: null, recordedCommit: null, checkedOutCommit: null, CancellationToken.None);

await Assert.That(readiness.Problems).IsEquivalentTo(
[$"The pinned server source is missing at '{entry}'. Run: git submodule update --init --depth 1 external/opencode"], CollectionOrdering.Matching);
}

[Test]
public async Task FindAsync_Should_Name_A_Mismatch_Typo_Before_Missing_Source()
{
var fileSystem = new MockFileSystem();
var root = Root(fileSystem);
var entry = fileSystem.Path.Combine(root, "external", "opencode", "packages", "cli", "src", "index.ts");

var readiness = await FindWithMatchingCommitsAsync(fileSystem, root, PinnedBun, "Allow");

await Assert.That(readiness.Problems).IsEquivalentTo([
"OPENCODE_SDK_TESTS_BUN_MISMATCH is 'Allow', but the only value it takes is 'allow'. Unset it to require the pinned bun.",
$"The pinned server source is missing at '{entry}'. Run: git submodule update --init --depth 1 external/opencode",
], CollectionOrdering.Matching);
}

private static Task<PinnedServerPrerequisites.Readiness> FindWithMatchingCommitsAsync(
MockFileSystem fileSystem, string root, string? bunVersion, string? bunMismatch) =>
PinnedServerPrerequisites.FindAsync(fileSystem, root, bunVersion, bunMismatch, RecordedCommit, RecordedCommit, CancellationToken.None);

private static string Root(MockFileSystem fileSystem) =>
fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), "repository");

Expand Down
2 changes: 0 additions & 2 deletions tools/jscpd/tests.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@
"479fdc49d7da2d6e": 1,
"47c6dec036a94d60": 1,
"47d341670708569a": 1,
"482c654299e74350": 1,
"4889b45748ec205e": 1,
"48c32c7eaffb48de": 1,
"48c83373cc2d7d8d": 1,
Expand Down Expand Up @@ -208,7 +207,6 @@
"567e66ef84568376": 2,
"5684435994da0e61": 1,
"573c75d83c9a3c95": 1,
"57470feca6bbb65f": 1,
"57f025d61c1ae125": 1,
"580bb3a45cc1aa00": 1,
"5890b6797611cdbf": 1,
Expand Down
Loading