audit notes work completed, test fixes work (95% done), new sprints, new data sources setup and configuration
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
namespace StellaOps.Interop.Tests.CycloneDx;
|
||||
|
||||
using Xunit.Sdk;
|
||||
|
||||
[Trait("Category", "Interop")]
|
||||
[Trait("Format", "CycloneDX")]
|
||||
public class CycloneDxRoundTripTests : IClassFixture<InteropTestHarness>
|
||||
@@ -78,8 +80,7 @@ public class CycloneDxRoundTripTests : IClassFixture<InteropTestHarness>
|
||||
// Skip if not in CI - cosign requires credentials
|
||||
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")))
|
||||
{
|
||||
// Skip in local dev
|
||||
return;
|
||||
throw SkipException.ForSkip("Cosign attestation requires CI credentials.");
|
||||
}
|
||||
|
||||
// Generate SBOM
|
||||
|
||||
@@ -3,6 +3,8 @@ namespace StellaOps.Interop.Tests;
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using StellaOps.Interop;
|
||||
using Xunit.Sdk;
|
||||
|
||||
/// <summary>
|
||||
/// Test harness for SBOM interoperability testing.
|
||||
@@ -12,6 +14,7 @@ public sealed class InteropTestHarness : IAsyncLifetime
|
||||
{
|
||||
private readonly ToolManager _toolManager;
|
||||
private readonly string _workDir;
|
||||
private string? _skipReason;
|
||||
|
||||
public InteropTestHarness()
|
||||
{
|
||||
@@ -23,6 +26,20 @@ public sealed class InteropTestHarness : IAsyncLifetime
|
||||
{
|
||||
Directory.CreateDirectory(_workDir);
|
||||
|
||||
var missingTools = new List<string>();
|
||||
if (!_toolManager.IsToolAvailable("syft"))
|
||||
missingTools.Add("syft");
|
||||
if (!_toolManager.IsToolAvailable("grype"))
|
||||
missingTools.Add("grype");
|
||||
if (!_toolManager.IsToolAvailable("cosign"))
|
||||
missingTools.Add("cosign");
|
||||
|
||||
if (missingTools.Count > 0)
|
||||
{
|
||||
_skipReason = $"Interop tools missing: {string.Join(", ", missingTools)}";
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify tools are available
|
||||
await _toolManager.VerifyToolAsync("syft", "--version");
|
||||
await _toolManager.VerifyToolAsync("grype", "--version");
|
||||
@@ -37,6 +54,7 @@ public sealed class InteropTestHarness : IAsyncLifetime
|
||||
SbomFormat format,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
EnsureToolsAvailable();
|
||||
var formatArg = format switch
|
||||
{
|
||||
SbomFormat.CycloneDx16 => "cyclonedx-json",
|
||||
@@ -72,6 +90,7 @@ public sealed class InteropTestHarness : IAsyncLifetime
|
||||
SbomFormat format,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
EnsureToolsAvailable();
|
||||
var formatArg = format switch
|
||||
{
|
||||
SbomFormat.CycloneDx16 => "cyclonedx",
|
||||
@@ -107,6 +126,7 @@ public sealed class InteropTestHarness : IAsyncLifetime
|
||||
string imageRef,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
EnsureToolsAvailable();
|
||||
var result = await _toolManager.RunAsync(
|
||||
"cosign",
|
||||
$"attest --predicate {sbomPath} --type cyclonedx {imageRef} --yes",
|
||||
@@ -125,6 +145,7 @@ public sealed class InteropTestHarness : IAsyncLifetime
|
||||
string sbomPath,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
EnsureToolsAvailable();
|
||||
var outputPath = Path.Combine(_workDir, "grype-findings.json");
|
||||
var result = await _toolManager.RunAsync(
|
||||
"grype",
|
||||
@@ -196,6 +217,12 @@ public sealed class InteropTestHarness : IAsyncLifetime
|
||||
// For now, return empty list
|
||||
return Array.Empty<GrypeFinding>();
|
||||
}
|
||||
|
||||
private void EnsureToolsAvailable()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_skipReason))
|
||||
throw SkipException.ForSkip(_skipReason);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,13 +6,18 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>StellaOps.Interop.Tests</RootNamespace>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\\..\\..\\__Libraries\\StellaOps.Interop\\StellaOps.Interop.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="FluentAssertions" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -8,3 +8,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| AUDIT-0371-M | DONE | Revalidated 2026-01-07; maintainability audit for StellaOps.Interop.Tests. |
|
||||
| AUDIT-0371-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Interop.Tests. |
|
||||
| AUDIT-0371-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
| AUDIT-TESTGAP-CORELIB-INTEROP-0001 | DONE | Added ToolManager unit tests + skip gating (2026-01-13). |
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ToolManager.cs
|
||||
// Sprint: SPRINT_5100_0003_0001_sbom_interop_roundtrip
|
||||
// Task: T1 - Interop Test Harness
|
||||
// Description: Manages execution of external tools (Syft, Grype, cosign).
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace StellaOps.Interop.Tests;
|
||||
|
||||
public sealed class ToolManager
|
||||
{
|
||||
private readonly string _workDir;
|
||||
|
||||
public ToolManager(string workDir)
|
||||
{
|
||||
_workDir = workDir;
|
||||
}
|
||||
|
||||
public async Task VerifyToolAsync(string tool, string versionArg)
|
||||
{
|
||||
var result = await RunAsync(tool, versionArg, CancellationToken.None);
|
||||
if (!result.Success)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Tool '{tool}' is not available or failed verification: {result.Error}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ToolResult> RunAsync(
|
||||
string tool,
|
||||
string arguments,
|
||||
CancellationToken ct,
|
||||
int timeoutSeconds = 300)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = tool,
|
||||
Arguments = arguments,
|
||||
WorkingDirectory = _workDir,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
var outputTask = process.StandardOutput.ReadToEndAsync(ct);
|
||||
var errorTask = process.StandardError.ReadToEndAsync(ct);
|
||||
|
||||
var completed = await Task.WhenAny(
|
||||
process.WaitForExitAsync(ct),
|
||||
Task.Delay(TimeSpan.FromSeconds(timeoutSeconds), ct));
|
||||
|
||||
if (!process.HasExited)
|
||||
{
|
||||
process.Kill(entireProcessTree: true);
|
||||
return new ToolResult(false, "", "Process timed out");
|
||||
}
|
||||
|
||||
var output = await outputTask;
|
||||
var error = await errorTask;
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
return new ToolResult(false, output, error);
|
||||
}
|
||||
|
||||
return new ToolResult(true, output);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ToolResult(false, "", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
158
src/__Tests/interop/StellaOps.Interop.Tests/ToolManagerTests.cs
Normal file
158
src/__Tests/interop/StellaOps.Interop.Tests/ToolManagerTests.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using System.Text;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Interop;
|
||||
using Xunit;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace StellaOps.Interop.Tests;
|
||||
|
||||
public sealed class ToolManagerTests
|
||||
{
|
||||
[Fact]
|
||||
public void ResolveToolPath_UsesConfiguredPath()
|
||||
{
|
||||
var workDir = CreateTempDirectory();
|
||||
try
|
||||
{
|
||||
var toolPath = Path.Combine(workDir, OperatingSystem.IsWindows() ? "stub-tool.exe" : "stub-tool");
|
||||
File.WriteAllText(toolPath, "stub", Encoding.ASCII);
|
||||
|
||||
var toolPaths = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["stub-tool"] = toolPath
|
||||
};
|
||||
|
||||
var manager = new ToolManager(workDir, toolPaths);
|
||||
|
||||
manager.ResolveToolPath("stub-tool").Should().Be(toolPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DeleteDirectory(workDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveToolPath_UsesPathProbe()
|
||||
{
|
||||
var workDir = CreateTempDirectory();
|
||||
var toolDir = CreateTempDirectory();
|
||||
var originalPath = Environment.GetEnvironmentVariable("PATH");
|
||||
try
|
||||
{
|
||||
var toolFile = OperatingSystem.IsWindows() ? "probe-tool.exe" : "probe-tool";
|
||||
var toolPath = Path.Combine(toolDir, toolFile);
|
||||
File.WriteAllText(toolPath, "stub", Encoding.ASCII);
|
||||
|
||||
Environment.SetEnvironmentVariable("PATH", toolDir);
|
||||
|
||||
var manager = new ToolManager(workDir);
|
||||
manager.ResolveToolPath("probe-tool").Should().Be(toolPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("PATH", originalPath);
|
||||
DeleteDirectory(toolDir);
|
||||
DeleteDirectory(workDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindOnPath_ReturnsNull_WhenPathMissing()
|
||||
{
|
||||
var originalPath = Environment.GetEnvironmentVariable("PATH");
|
||||
try
|
||||
{
|
||||
Environment.SetEnvironmentVariable("PATH", string.Empty);
|
||||
|
||||
ToolManager.FindOnPath("missing-tool").Should().BeNull();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("PATH", originalPath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_ReturnsFailure_WhenToolMissing()
|
||||
{
|
||||
var workDir = CreateTempDirectory();
|
||||
try
|
||||
{
|
||||
var manager = new ToolManager(workDir);
|
||||
|
||||
var result = await manager.RunAsync("missing-tool", "--version", CancellationToken.None);
|
||||
|
||||
result.Success.Should().BeFalse();
|
||||
result.Error.Should().Contain("Tool not found");
|
||||
result.ExitCode.Should().Be(-1);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DeleteDirectory(workDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_ReturnsSuccess_WhenShellExecutesScript()
|
||||
{
|
||||
var workDir = CreateTempDirectory();
|
||||
try
|
||||
{
|
||||
var scriptPath = WriteShellScript(workDir);
|
||||
var shellPath = ResolveShellPath();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(shellPath) || !File.Exists(shellPath))
|
||||
throw SkipException.ForSkip("Shell not available for interop tool test.");
|
||||
|
||||
var toolPaths = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["shell"] = shellPath
|
||||
};
|
||||
|
||||
var args = OperatingSystem.IsWindows()
|
||||
? $"/c \"{scriptPath}\""
|
||||
: $"\"{scriptPath}\"";
|
||||
|
||||
var manager = new ToolManager(workDir, toolPaths);
|
||||
var result = await manager.RunAsync("shell", args, CancellationToken.None);
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.StdOut.Should().Contain("ok");
|
||||
}
|
||||
finally
|
||||
{
|
||||
DeleteDirectory(workDir);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveShellPath()
|
||||
=> OperatingSystem.IsWindows()
|
||||
? Environment.GetEnvironmentVariable("ComSpec") ?? string.Empty
|
||||
: "/bin/sh";
|
||||
|
||||
private static string WriteShellScript(string directory)
|
||||
{
|
||||
var scriptName = OperatingSystem.IsWindows() ? "interop-tool.cmd" : "interop-tool.sh";
|
||||
var scriptPath = Path.Combine(directory, scriptName);
|
||||
var content = OperatingSystem.IsWindows()
|
||||
? "@echo off\r\necho ok\r\nexit /b 0\r\n"
|
||||
: "#!/bin/sh\n\necho ok\nexit 0\n";
|
||||
|
||||
File.WriteAllText(scriptPath, content, Encoding.ASCII);
|
||||
return scriptPath;
|
||||
}
|
||||
|
||||
private static string CreateTempDirectory()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"interop-tool-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
private static void DeleteDirectory(string path)
|
||||
{
|
||||
if (Directory.Exists(path))
|
||||
Directory.Delete(path, recursive: true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user