Use package-lock.json to pin conformance test dependencies (#1247)
This commit is contained in:
parent
cfa89a3907
commit
e55ae7b7bd
1 changed files with 1416 additions and 54 deletions
|
|
@ -8,20 +8,88 @@ namespace ModelContextProtocol.Tests.Utils;
|
|||
/// </summary>
|
||||
public static class NodeHelpers
|
||||
{
|
||||
private static readonly object _npmInstallLock = new();
|
||||
private static bool _npmInstallCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a ProcessStartInfo configured to run npx with the specified arguments.
|
||||
/// Finds the repository root by searching for package-lock.json in ancestor directories.
|
||||
/// </summary>
|
||||
/// <param name="arguments">The arguments to pass to npx.</param>
|
||||
/// <returns>A configured ProcessStartInfo for running npx.</returns>
|
||||
public static ProcessStartInfo NpxStartInfo(string arguments)
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
var dir = AppContext.BaseDirectory;
|
||||
while (dir is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(dir, "package-lock.json")))
|
||||
{
|
||||
return dir;
|
||||
}
|
||||
dir = Path.GetDirectoryName(dir);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Could not find repository root (no package-lock.json found in ancestor directories).");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures npm dependencies are installed by running 'npm ci' from the repo root.
|
||||
/// This is safe to call multiple times; it only runs once per test process.
|
||||
/// </summary>
|
||||
public static void EnsureNpmDependenciesInstalled()
|
||||
{
|
||||
if (_npmInstallCompleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_npmInstallLock)
|
||||
{
|
||||
if (_npmInstallCompleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var repoRoot = FindRepoRoot();
|
||||
var nodeModulesPath = Path.Combine(repoRoot, "node_modules");
|
||||
|
||||
// Use 'npm ci' if node_modules doesn't exist, otherwise assume it's up to date.
|
||||
if (!Directory.Exists(nodeModulesPath))
|
||||
{
|
||||
var startInfo = NpmStartInfo("ci", repoRoot);
|
||||
using var process = Process.Start(startInfo)
|
||||
?? throw new InvalidOperationException("Failed to start 'npm ci'.");
|
||||
process.WaitForExit(120_000);
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
var error = process.StandardError.ReadToEnd();
|
||||
throw new InvalidOperationException($"'npm ci' failed with exit code {process.ExitCode}: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
_npmInstallCompleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a ProcessStartInfo configured to run a binary from node_modules/.bin/conformance.
|
||||
/// Calls <see cref="EnsureNpmDependenciesInstalled"/> first.
|
||||
/// </summary>
|
||||
/// <param name="binaryName">The name of the binary in node_modules/.bin (e.g. "conformance").</param>
|
||||
/// <param name="arguments">The arguments to pass to the binary.</param>
|
||||
/// <returns>A configured ProcessStartInfo for running the binary.</returns>
|
||||
public static ProcessStartInfo ConformanceTestStartInfo(string arguments)
|
||||
{
|
||||
EnsureNpmDependenciesInstalled();
|
||||
|
||||
var repoRoot = FindRepoRoot();
|
||||
var binPath = Path.Combine(repoRoot, "node_modules", ".bin", "conformance");
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// On Windows, npx is a PowerShell script, so we need to use cmd.exe to invoke it
|
||||
// On Windows, node_modules/.bin contains .cmd shims that can be executed directly
|
||||
return new ProcessStartInfo
|
||||
{
|
||||
FileName = "cmd.exe",
|
||||
Arguments = $"/c npx {arguments}",
|
||||
FileName = $"{binPath}.cmd",
|
||||
Arguments = arguments,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
|
|
@ -30,10 +98,9 @@ public static class NodeHelpers
|
|||
}
|
||||
else
|
||||
{
|
||||
// On Unix-like systems, npx is typically a shell script that can be executed directly
|
||||
return new ProcessStartInfo
|
||||
{
|
||||
FileName = "npx",
|
||||
FileName = binPath,
|
||||
Arguments = arguments,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
|
|
@ -44,16 +111,22 @@ public static class NodeHelpers
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if Node.js and npx are installed and available on the system.
|
||||
/// Checks if Node.js is installed and available on the system.
|
||||
/// </summary>
|
||||
/// <returns>True if npx is available, false otherwise.</returns>
|
||||
public static bool IsNpxInstalled()
|
||||
public static bool IsNodeInstalled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var startInfo = NpxStartInfo("--version");
|
||||
using var process = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "node",
|
||||
Arguments = "--version",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
});
|
||||
|
||||
using var process = Process.Start(startInfo);
|
||||
if (process == null)
|
||||
{
|
||||
return false;
|
||||
|
|
@ -67,4 +140,34 @@ public static class NodeHelpers
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static ProcessStartInfo NpmStartInfo(string arguments, string workingDirectory)
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return new ProcessStartInfo
|
||||
{
|
||||
FileName = "cmd.exe",
|
||||
Arguments = $"/c npm {arguments}",
|
||||
WorkingDirectory = workingDirectory,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ProcessStartInfo
|
||||
{
|
||||
FileName = "npm",
|
||||
Arguments = arguments,
|
||||
WorkingDirectory = workingDirectory,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue