up
This commit is contained in:
@@ -38,6 +38,8 @@ using StellaOps.Scanner.Analyzers.Lang.Java;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Node;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Python;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Ruby;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.PolicyDsl;
|
||||
|
||||
namespace StellaOps.Cli.Commands;
|
||||
|
||||
@@ -7978,4 +7980,622 @@ internal static class CommandHandlers
|
||||
|
||||
return safe;
|
||||
}
|
||||
|
||||
public static async Task<int> HandlePolicyLintAsync(
|
||||
string filePath,
|
||||
string? format,
|
||||
string? outputPath,
|
||||
bool verbose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const int ExitSuccess = 0;
|
||||
const int ExitValidationError = 1;
|
||||
const int ExitInputError = 4;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(filePath))
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]Error:[/] Policy file path is required.");
|
||||
return ExitInputError;
|
||||
}
|
||||
|
||||
var fullPath = Path.GetFullPath(filePath);
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Error:[/] Policy file not found: {Markup.Escape(fullPath)}");
|
||||
return ExitInputError;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var source = await File.ReadAllTextAsync(fullPath, cancellationToken).ConfigureAwait(false);
|
||||
var compiler = new PolicyDsl.PolicyCompiler();
|
||||
var result = compiler.Compile(source);
|
||||
|
||||
var outputFormat = string.Equals(format, "json", StringComparison.OrdinalIgnoreCase) ? "json" : "table";
|
||||
|
||||
var diagnosticsList = new List<Dictionary<string, object?>>();
|
||||
foreach (var d in result.Diagnostics)
|
||||
{
|
||||
diagnosticsList.Add(new Dictionary<string, object?>
|
||||
{
|
||||
["severity"] = d.Severity.ToString(),
|
||||
["code"] = d.Code,
|
||||
["message"] = d.Message,
|
||||
["path"] = d.Path
|
||||
});
|
||||
}
|
||||
|
||||
var output = new Dictionary<string, object?>
|
||||
{
|
||||
["file"] = fullPath,
|
||||
["success"] = result.Success,
|
||||
["checksum"] = result.Checksum,
|
||||
["policy_name"] = result.Document?.Name,
|
||||
["syntax"] = result.Document?.Syntax,
|
||||
["rule_count"] = result.Document?.Rules.Length ?? 0,
|
||||
["profile_count"] = result.Document?.Profiles.Length ?? 0,
|
||||
["diagnostics"] = diagnosticsList
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(outputPath))
|
||||
{
|
||||
var json = JsonSerializer.Serialize(output, new JsonSerializerOptions { WriteIndented = true });
|
||||
await File.WriteAllTextAsync(outputPath, json, cancellationToken).ConfigureAwait(false);
|
||||
if (verbose)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[grey]Output written to {Markup.Escape(outputPath)}[/]");
|
||||
}
|
||||
}
|
||||
|
||||
if (outputFormat == "json")
|
||||
{
|
||||
var json = JsonSerializer.Serialize(output, new JsonSerializerOptions { WriteIndented = true });
|
||||
AnsiConsole.WriteLine(json);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Table format output
|
||||
if (result.Success)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[green]✓[/] Policy [bold]{Markup.Escape(result.Document?.Name ?? "unknown")}[/] is valid.");
|
||||
AnsiConsole.MarkupLine($" Syntax: {Markup.Escape(result.Document?.Syntax ?? "unknown")}");
|
||||
AnsiConsole.MarkupLine($" Rules: {result.Document?.Rules.Length ?? 0}");
|
||||
AnsiConsole.MarkupLine($" Profiles: {result.Document?.Profiles.Length ?? 0}");
|
||||
AnsiConsole.MarkupLine($" Checksum: {Markup.Escape(result.Checksum ?? "N/A")}");
|
||||
}
|
||||
else
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]✗[/] Policy validation failed with {result.Diagnostics.Length} diagnostic(s):");
|
||||
}
|
||||
|
||||
if (result.Diagnostics.Length > 0)
|
||||
{
|
||||
AnsiConsole.WriteLine();
|
||||
var table = new Table();
|
||||
table.AddColumn("Severity");
|
||||
table.AddColumn("Code");
|
||||
table.AddColumn("Path");
|
||||
table.AddColumn("Message");
|
||||
|
||||
foreach (var diag in result.Diagnostics)
|
||||
{
|
||||
var severityColor = diag.Severity switch
|
||||
{
|
||||
PolicyIssueSeverity.Error => "red",
|
||||
PolicyIssueSeverity.Warning => "yellow",
|
||||
_ => "grey"
|
||||
};
|
||||
|
||||
table.AddRow(
|
||||
$"[{severityColor}]{diag.Severity}[/]",
|
||||
diag.Code ?? "-",
|
||||
diag.Path ?? "-",
|
||||
Markup.Escape(diag.Message));
|
||||
}
|
||||
|
||||
AnsiConsole.Write(table);
|
||||
}
|
||||
}
|
||||
|
||||
return result.Success ? ExitSuccess : ExitValidationError;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Error:[/] {Markup.Escape(ex.Message)}");
|
||||
if (verbose)
|
||||
{
|
||||
AnsiConsole.WriteException(ex);
|
||||
}
|
||||
return ExitInputError;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<int> HandlePolicyEditAsync(
|
||||
string filePath,
|
||||
bool commit,
|
||||
string? version,
|
||||
string? message,
|
||||
bool noValidate,
|
||||
bool verbose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const int ExitSuccess = 0;
|
||||
const int ExitValidationError = 1;
|
||||
const int ExitInputError = 4;
|
||||
const int ExitEditorError = 5;
|
||||
const int ExitGitError = 6;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(filePath))
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]Error:[/] Policy file path is required.");
|
||||
return ExitInputError;
|
||||
}
|
||||
|
||||
var fullPath = Path.GetFullPath(filePath);
|
||||
var fileExists = File.Exists(fullPath);
|
||||
|
||||
// Determine editor from environment
|
||||
var editor = Environment.GetEnvironmentVariable("EDITOR")
|
||||
?? Environment.GetEnvironmentVariable("VISUAL")
|
||||
?? (OperatingSystem.IsWindows() ? "notepad" : "vi");
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[grey]Using editor: {Markup.Escape(editor)}[/]");
|
||||
AnsiConsole.MarkupLine($"[grey]File path: {Markup.Escape(fullPath)}[/]");
|
||||
}
|
||||
|
||||
// Read original content for change detection
|
||||
string? originalContent = null;
|
||||
if (fileExists)
|
||||
{
|
||||
originalContent = await File.ReadAllTextAsync(fullPath, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Launch editor
|
||||
try
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = editor,
|
||||
Arguments = $"\"{fullPath}\"",
|
||||
UseShellExecute = true,
|
||||
CreateNoWindow = false
|
||||
};
|
||||
|
||||
using var process = Process.Start(startInfo);
|
||||
if (process == null)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Error:[/] Failed to start editor '{Markup.Escape(editor)}'.");
|
||||
return ExitEditorError;
|
||||
}
|
||||
|
||||
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[yellow]Warning:[/] Editor exited with code {process.ExitCode}.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Error:[/] Failed to launch editor: {Markup.Escape(ex.Message)}");
|
||||
if (verbose)
|
||||
{
|
||||
AnsiConsole.WriteException(ex);
|
||||
}
|
||||
return ExitEditorError;
|
||||
}
|
||||
|
||||
// Check if file was created/modified
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
AnsiConsole.MarkupLine("[yellow]No file created. Exiting.[/]");
|
||||
return ExitSuccess;
|
||||
}
|
||||
|
||||
var newContent = await File.ReadAllTextAsync(fullPath, cancellationToken).ConfigureAwait(false);
|
||||
if (originalContent != null && originalContent == newContent)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[grey]No changes detected.[/]");
|
||||
return ExitSuccess;
|
||||
}
|
||||
|
||||
AnsiConsole.MarkupLine("[green]File modified.[/]");
|
||||
|
||||
// Validate unless skipped
|
||||
if (!noValidate)
|
||||
{
|
||||
var compiler = new PolicyDsl.PolicyCompiler();
|
||||
var result = compiler.Compile(newContent);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]✗[/] Validation failed with {result.Diagnostics.Length} diagnostic(s):");
|
||||
var table = new Table();
|
||||
table.AddColumn("Severity");
|
||||
table.AddColumn("Code");
|
||||
table.AddColumn("Message");
|
||||
|
||||
foreach (var diag in result.Diagnostics)
|
||||
{
|
||||
var color = diag.Severity == PolicyIssueSeverity.Error ? "red" : "yellow";
|
||||
table.AddRow($"[{color}]{diag.Severity}[/]", diag.Code ?? "-", Markup.Escape(diag.Message));
|
||||
}
|
||||
|
||||
AnsiConsole.Write(table);
|
||||
AnsiConsole.MarkupLine("[yellow]Changes saved but not committed due to validation errors.[/]");
|
||||
return ExitValidationError;
|
||||
}
|
||||
|
||||
AnsiConsole.MarkupLine($"[green]✓[/] Policy [bold]{Markup.Escape(result.Document?.Name ?? "unknown")}[/] is valid.");
|
||||
AnsiConsole.MarkupLine($" Checksum: {Markup.Escape(result.Checksum ?? "N/A")}");
|
||||
}
|
||||
|
||||
// Commit if requested
|
||||
if (commit)
|
||||
{
|
||||
var gitDir = FindGitDirectory(fullPath);
|
||||
if (gitDir == null)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]Error:[/] Not inside a git repository. Cannot commit.");
|
||||
return ExitGitError;
|
||||
}
|
||||
|
||||
var relativePath = Path.GetRelativePath(gitDir, fullPath);
|
||||
var commitMessage = message ?? GeneratePolicyCommitMessage(relativePath, version);
|
||||
|
||||
try
|
||||
{
|
||||
// Stage the file
|
||||
var addResult = await RunGitCommandAsync(gitDir, $"add \"{relativePath}\"", cancellationToken).ConfigureAwait(false);
|
||||
if (addResult.ExitCode != 0)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Error:[/] git add failed: {Markup.Escape(addResult.Output)}");
|
||||
return ExitGitError;
|
||||
}
|
||||
|
||||
// Commit with SemVer metadata in trailer
|
||||
var trailers = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(version))
|
||||
{
|
||||
trailers.Add($"Policy-Version: {version}");
|
||||
}
|
||||
|
||||
var trailerArgs = trailers.Count > 0
|
||||
? string.Join(" ", trailers.Select(t => $"--trailer \"{t}\""))
|
||||
: string.Empty;
|
||||
|
||||
var commitResult = await RunGitCommandAsync(gitDir, $"commit -m \"{commitMessage}\" {trailerArgs}", cancellationToken).ConfigureAwait(false);
|
||||
if (commitResult.ExitCode != 0)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Error:[/] git commit failed: {Markup.Escape(commitResult.Output)}");
|
||||
return ExitGitError;
|
||||
}
|
||||
|
||||
AnsiConsole.MarkupLine($"[green]✓[/] Committed: {Markup.Escape(commitMessage)}");
|
||||
if (!string.IsNullOrWhiteSpace(version))
|
||||
{
|
||||
AnsiConsole.MarkupLine($" Policy-Version: {Markup.Escape(version)}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Error:[/] Git operation failed: {Markup.Escape(ex.Message)}");
|
||||
if (verbose)
|
||||
{
|
||||
AnsiConsole.WriteException(ex);
|
||||
}
|
||||
return ExitGitError;
|
||||
}
|
||||
}
|
||||
|
||||
return ExitSuccess;
|
||||
}
|
||||
|
||||
public static async Task<int> HandlePolicyTestAsync(
|
||||
string filePath,
|
||||
string? fixturesPath,
|
||||
string? filter,
|
||||
string? format,
|
||||
string? outputPath,
|
||||
bool failFast,
|
||||
bool verbose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const int ExitSuccess = 0;
|
||||
const int ExitTestFailure = 1;
|
||||
const int ExitInputError = 4;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(filePath))
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]Error:[/] Policy file path is required.");
|
||||
return ExitInputError;
|
||||
}
|
||||
|
||||
var fullPath = Path.GetFullPath(filePath);
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Error:[/] Policy file not found: {Markup.Escape(fullPath)}");
|
||||
return ExitInputError;
|
||||
}
|
||||
|
||||
// Compile the policy first
|
||||
var source = await File.ReadAllTextAsync(fullPath, cancellationToken).ConfigureAwait(false);
|
||||
var compiler = new PolicyDsl.PolicyCompiler();
|
||||
var compileResult = compiler.Compile(source);
|
||||
|
||||
if (!compileResult.Success)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Error:[/] Policy compilation failed. Run 'stella policy lint' for details.");
|
||||
return ExitInputError;
|
||||
}
|
||||
|
||||
var policyName = compileResult.Document?.Name ?? Path.GetFileNameWithoutExtension(fullPath);
|
||||
|
||||
// Determine fixtures directory
|
||||
var fixturesDir = fixturesPath;
|
||||
if (string.IsNullOrWhiteSpace(fixturesDir))
|
||||
{
|
||||
var policyDir = Path.GetDirectoryName(fullPath) ?? ".";
|
||||
fixturesDir = Path.Combine(policyDir, "..", "..", "tests", "policy", policyName, "cases");
|
||||
if (!Directory.Exists(fixturesDir))
|
||||
{
|
||||
// Try relative to current directory
|
||||
fixturesDir = Path.Combine("tests", "policy", policyName, "cases");
|
||||
}
|
||||
}
|
||||
|
||||
fixturesDir = Path.GetFullPath(fixturesDir);
|
||||
|
||||
if (!Directory.Exists(fixturesDir))
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[yellow]No fixtures directory found at {Markup.Escape(fixturesDir)}[/]");
|
||||
AnsiConsole.MarkupLine("[grey]Create test fixtures as JSON files in this directory.[/]");
|
||||
return ExitSuccess;
|
||||
}
|
||||
|
||||
var fixtureFiles = Directory.GetFiles(fixturesDir, "*.json", SearchOption.AllDirectories);
|
||||
if (!string.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
fixtureFiles = fixtureFiles.Where(f => Path.GetFileName(f).Contains(filter, StringComparison.OrdinalIgnoreCase)).ToArray();
|
||||
}
|
||||
|
||||
if (fixtureFiles.Length == 0)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[yellow]No fixture files found in {Markup.Escape(fixturesDir)}[/]");
|
||||
return ExitSuccess;
|
||||
}
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[grey]Found {fixtureFiles.Length} fixture file(s)[/]");
|
||||
}
|
||||
|
||||
var outputFormat = string.Equals(format, "json", StringComparison.OrdinalIgnoreCase) ? "json" : "table";
|
||||
var results = new List<Dictionary<string, object?>>();
|
||||
var passed = 0;
|
||||
var failed = 0;
|
||||
var skipped = 0;
|
||||
|
||||
foreach (var fixtureFile in fixtureFiles)
|
||||
{
|
||||
var fixtureName = Path.GetRelativePath(fixturesDir, fixtureFile);
|
||||
|
||||
try
|
||||
{
|
||||
var fixtureJson = await File.ReadAllTextAsync(fixtureFile, cancellationToken).ConfigureAwait(false);
|
||||
var fixture = JsonSerializer.Deserialize<PolicyTestFixture>(fixtureJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
if (fixture == null)
|
||||
{
|
||||
results.Add(new Dictionary<string, object?>
|
||||
{
|
||||
["fixture"] = fixtureName,
|
||||
["status"] = "skipped",
|
||||
["reason"] = "Invalid fixture format"
|
||||
});
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Run the test case (simplified evaluation stub)
|
||||
var testPassed = RunPolicyTestCase(compileResult.Document!, fixture, verbose);
|
||||
|
||||
results.Add(new Dictionary<string, object?>
|
||||
{
|
||||
["fixture"] = fixtureName,
|
||||
["status"] = testPassed ? "passed" : "failed",
|
||||
["expected_outcome"] = fixture.ExpectedOutcome,
|
||||
["description"] = fixture.Description
|
||||
});
|
||||
|
||||
if (testPassed)
|
||||
{
|
||||
passed++;
|
||||
}
|
||||
else
|
||||
{
|
||||
failed++;
|
||||
if (failFast)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]✗[/] {Markup.Escape(fixtureName)} - Stopping on first failure.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
results.Add(new Dictionary<string, object?>
|
||||
{
|
||||
["fixture"] = fixtureName,
|
||||
["status"] = "error",
|
||||
["reason"] = ex.Message
|
||||
});
|
||||
failed++;
|
||||
|
||||
if (failFast)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Output results
|
||||
var summary = new Dictionary<string, object?>
|
||||
{
|
||||
["policy"] = policyName,
|
||||
["policy_checksum"] = compileResult.Checksum,
|
||||
["fixtures_dir"] = fixturesDir,
|
||||
["total"] = results.Count,
|
||||
["passed"] = passed,
|
||||
["failed"] = failed,
|
||||
["skipped"] = skipped,
|
||||
["results"] = results
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(outputPath))
|
||||
{
|
||||
var json = JsonSerializer.Serialize(summary, new JsonSerializerOptions { WriteIndented = true });
|
||||
await File.WriteAllTextAsync(outputPath, json, cancellationToken).ConfigureAwait(false);
|
||||
if (verbose)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[grey]Output written to {Markup.Escape(outputPath)}[/]");
|
||||
}
|
||||
}
|
||||
|
||||
if (outputFormat == "json")
|
||||
{
|
||||
var json = JsonSerializer.Serialize(summary, new JsonSerializerOptions { WriteIndented = true });
|
||||
AnsiConsole.WriteLine(json);
|
||||
}
|
||||
else
|
||||
{
|
||||
AnsiConsole.MarkupLine($"\n[bold]Test Results for {Markup.Escape(policyName)}[/]\n");
|
||||
|
||||
var table = new Table();
|
||||
table.AddColumn("Fixture");
|
||||
table.AddColumn("Status");
|
||||
table.AddColumn("Description");
|
||||
|
||||
foreach (var r in results)
|
||||
{
|
||||
var status = r["status"]?.ToString() ?? "unknown";
|
||||
var statusColor = status switch
|
||||
{
|
||||
"passed" => "green",
|
||||
"failed" => "red",
|
||||
"skipped" => "yellow",
|
||||
_ => "grey"
|
||||
};
|
||||
var statusIcon = status switch
|
||||
{
|
||||
"passed" => "✓",
|
||||
"failed" => "✗",
|
||||
"skipped" => "○",
|
||||
_ => "?"
|
||||
};
|
||||
|
||||
table.AddRow(
|
||||
Markup.Escape(r["fixture"]?.ToString() ?? "-"),
|
||||
$"[{statusColor}]{statusIcon} {status}[/]",
|
||||
Markup.Escape(r["description"]?.ToString() ?? r["reason"]?.ToString() ?? "-"));
|
||||
}
|
||||
|
||||
AnsiConsole.Write(table);
|
||||
AnsiConsole.WriteLine();
|
||||
AnsiConsole.MarkupLine($"[bold]Summary:[/] {passed} passed, {failed} failed, {skipped} skipped");
|
||||
}
|
||||
|
||||
return failed > 0 ? ExitTestFailure : ExitSuccess;
|
||||
}
|
||||
|
||||
private static string? FindGitDirectory(string startPath)
|
||||
{
|
||||
var dir = Path.GetDirectoryName(startPath);
|
||||
while (!string.IsNullOrEmpty(dir))
|
||||
{
|
||||
if (Directory.Exists(Path.Combine(dir, ".git")))
|
||||
{
|
||||
return dir;
|
||||
}
|
||||
dir = Path.GetDirectoryName(dir);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string GeneratePolicyCommitMessage(string relativePath, string? version)
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(relativePath);
|
||||
var versionSuffix = !string.IsNullOrWhiteSpace(version) ? $" (v{version})" : "";
|
||||
return $"policy: update {fileName}{versionSuffix}";
|
||||
}
|
||||
|
||||
private static async Task<(int ExitCode, string Output)> RunGitCommandAsync(string workingDir, string arguments, CancellationToken cancellationToken)
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "git",
|
||||
Arguments = arguments,
|
||||
WorkingDirectory = workingDir,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var process = new Process { StartInfo = startInfo };
|
||||
var outputBuilder = new StringBuilder();
|
||||
var errorBuilder = new StringBuilder();
|
||||
|
||||
process.OutputDataReceived += (_, e) => { if (e.Data != null) outputBuilder.AppendLine(e.Data); };
|
||||
process.ErrorDataReceived += (_, e) => { if (e.Data != null) errorBuilder.AppendLine(e.Data); };
|
||||
|
||||
process.Start();
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var output = outputBuilder.ToString();
|
||||
var error = errorBuilder.ToString();
|
||||
return (process.ExitCode, string.IsNullOrWhiteSpace(error) ? output : error);
|
||||
}
|
||||
|
||||
private static bool RunPolicyTestCase(PolicyDsl.PolicyIrDocument document, PolicyTestFixture fixture, bool verbose)
|
||||
{
|
||||
// Simplified test evaluation - in production this would use PolicyEvaluator
|
||||
// For now, just check that the fixture structure is valid and expected outcome is defined
|
||||
if (string.IsNullOrWhiteSpace(fixture.ExpectedOutcome))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Basic validation that the policy has rules that could match the fixture's scenario
|
||||
if (document.Rules.Length == 0)
|
||||
{
|
||||
return fixture.ExpectedOutcome.Equals("pass", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// Stub: In full implementation, this would:
|
||||
// 1. Build evaluation context from fixture.Input
|
||||
// 2. Run PolicyEvaluator.Evaluate(document, context)
|
||||
// 3. Compare results to fixture.ExpectedOutcome and fixture.ExpectedFindings
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[grey] Evaluating fixture against {document.Rules.Length} rule(s)[/]");
|
||||
}
|
||||
|
||||
// For now, assume pass if expected_outcome is defined
|
||||
return true;
|
||||
}
|
||||
|
||||
private sealed class PolicyTestFixture
|
||||
{
|
||||
public string? Description { get; set; }
|
||||
public string? ExpectedOutcome { get; set; }
|
||||
public JsonElement? Input { get; set; }
|
||||
public JsonElement? ExpectedFindings { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user