Files
gregExtractor/Commands/CoverageCommand.cs
T

148 lines
5.3 KiB
C#

using gregExtractor.Models;
using gregExtractor.Services;
using gregExtractor.Utils;
namespace gregExtractor.Commands;
public sealed class CoverageCommand : AsyncCommand<CoverageCommand.Settings>
{
public sealed class Settings : CommandSettings
{
[CommandOption("--path <IL2CPP_PATH>")]
public string? Path { get; init; }
[CommandOption("--hooks <GAME_HOOKS_JSON>")]
public string? HooksPath { get; init; }
[CommandOption("--sources <SOURCE_DIRS>")]
public string? SourceDirs { get; init; }
[CommandOption("--out <OUTPUT_BASE_PATH>")]
public string? OutputBasePath { get; init; }
[CommandOption("--open")]
public bool OpenAfterGenerate { get; init; }
}
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings)
{
AnsiConsole.Write(new FigletText("gregCoverage").Color(Color.Cyan1));
string workingDirectory = Directory.GetCurrentDirectory();
string? il2CppPath = settings.Path;
if (string.IsNullOrWhiteSpace(il2CppPath))
il2CppPath = SteamLocator.FindIl2CppAssembliesPath();
if (string.IsNullOrWhiteSpace(il2CppPath))
{
AnsiConsole.MarkupLine("[yellow]Could not auto-detect Il2CppAssemblies path.[/]");
il2CppPath = AnsiConsole.Ask<string>("Enter IL2CPP directory path:");
}
if (string.IsNullOrWhiteSpace(il2CppPath) || !Directory.Exists(il2CppPath))
{
AnsiConsole.MarkupLine("[red]Invalid IL2CPP path.[/]");
return -1;
}
string hooksPath = string.IsNullOrWhiteSpace(settings.HooksPath)
? Path.Combine(workingDirectory, "game_hooks.json")
: settings.HooksPath;
if (!File.Exists(hooksPath))
{
AnsiConsole.MarkupLine($"[red]Hooks file not found:[/] {Markup.Escape(hooksPath)}");
return -1;
}
string[] sourceDirs = CoveragePathResolver.ResolveSourceDirs(settings.SourceDirs, workingDirectory);
if (sourceDirs.Length == 0)
{
AnsiConsole.MarkupLine("[red]No valid framework source directories found.[/]");
AnsiConsole.MarkupLine("[grey]Use --sources \"<dir1>;<dir2>\"[/]");
return -1;
}
var analyzer = new CoverageAnalyzerService();
var messages = new List<string>();
var progress = new Progress<string>(message => messages.Add(message));
CoverageReport report = default!;
try
{
await AnsiConsole.Status()
.Spinner(Spinner.Known.Dots)
.StartAsync("Analyzing coverage...", async _ =>
{
report = await Task.Run(() => analyzer.Analyze(new AnalyzeOptions(
Il2CppDir: il2CppPath!,
GameHooksJsonPath: hooksPath,
FrameworkSourceDirs: sourceDirs,
OutputBasePath: settings.OutputBasePath,
Progress: progress)))
.ConfigureAwait(false);
})
.ConfigureAwait(false);
}
catch (Exception exception)
{
AnsiConsole.MarkupLine($"[red]Coverage analysis failed:[/] {Markup.Escape(exception.Message)}");
return -1;
}
foreach (string message in messages)
AnsiConsole.MarkupLine($"[grey]{Markup.Escape(message)}[/]");
var summary = new Table().RoundedBorder();
summary.AddColumn("Metric");
summary.AddColumn("Value");
summary.AddRow("Total Hooks", report.TotalHooks.ToString());
summary.AddRow("Covered", report.CoveredCount.ToString());
summary.AddRow("Planned", report.PlannedCount.ToString());
summary.AddRow("Uncovered", report.UncoveredCount.ToString());
summary.AddRow("Coverage", $"{report.CoveragePercent:F2}%");
AnsiConsole.Write(summary);
HookCoverageEntry[] uncoveredTop = report.Entries
.Where(entry => entry.CoverageStatus == CoverageStatus.Uncovered)
.Take(10)
.ToArray();
if (uncoveredTop.Length > 0)
{
var uncoveredTable = new Table().RoundedBorder();
uncoveredTable.Title = new TableTitle("Top 10 Uncovered");
uncoveredTable.AddColumn("Group");
uncoveredTable.AddColumn("Class");
uncoveredTable.AddColumn("Method");
foreach (HookCoverageEntry entry in uncoveredTop)
{
uncoveredTable.AddRow(
entry.HookDefinition.Group,
entry.HookDefinition.ClassName,
entry.HookDefinition.MethodName);
}
AnsiConsole.Write(uncoveredTable);
}
(string jsonPath, string mdPath) = CoverageAnalyzerService.GetReportPaths(settings.OutputBasePath);
AnsiConsole.MarkupLine($"[green]coverage_report.json[/] -> {Markup.Escape(jsonPath)}");
AnsiConsole.MarkupLine($"[green]coverage_report.md[/] -> {Markup.Escape(mdPath)}");
if (settings.OpenAfterGenerate && File.Exists(mdPath))
{
Process.Start(new ProcessStartInfo
{
FileName = mdPath,
UseShellExecute = true,
});
}
return 0;
}
}