namespace LiquidCode.Tester.Worker.Services; public class OutputCheckerService : IOutputCheckerService { private readonly ILogger _logger; private readonly CheckerService _checkerService; private readonly CheckerServiceIsolate _checkerServiceIsolate; private readonly bool _useIsolate; public OutputCheckerService( ILogger logger, CheckerService checkerService, CheckerServiceIsolate checkerServiceIsolate, IConfiguration configuration) { _logger = logger; _checkerService = checkerService; _checkerServiceIsolate = checkerServiceIsolate; _useIsolate = configuration.GetValue("Isolate:Enabled", false); if (_useIsolate) { _logger.LogInformation("Using Isolate sandbox for checker execution"); } } public async Task CheckOutputAsync(string actualOutput, string expectedOutputPath) { try { var expectedOutput = await File.ReadAllTextAsync(expectedOutputPath); // Normalize outputs for comparison var normalizedActual = NormalizeOutput(actualOutput); var normalizedExpected = NormalizeOutput(expectedOutput); var match = normalizedActual == normalizedExpected; if (!match) { _logger.LogDebug("Output mismatch. Expected length: {ExpectedLength}, Actual length: {ActualLength}", normalizedExpected.Length, normalizedActual.Length); } return match; } catch (Exception ex) { _logger.LogError(ex, "Error checking output against {ExpectedFile}", expectedOutputPath); return false; } } private string NormalizeOutput(string output) { // Remove trailing whitespace from each line and normalize line endings var lines = output.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None) .Select(line => line.TrimEnd()) .ToList(); // Remove trailing empty lines while (lines.Count > 0 && string.IsNullOrWhiteSpace(lines[^1])) { lines.RemoveAt(lines.Count - 1); } // Remove leading empty lines while (lines.Count > 0 && string.IsNullOrWhiteSpace(lines[0])) { lines.RemoveAt(0); } return string.Join("\n", lines); } public async Task CheckOutputWithCheckerAsync( string actualOutput, string inputFilePath, string expectedOutputPath, string? checkerPath) { // If custom checker is available, use it if (!string.IsNullOrEmpty(checkerPath) && File.Exists(checkerPath)) { _logger.LogDebug("Using custom checker: {CheckerPath} (Isolate: {UseIsolate})", checkerPath, _useIsolate); var checkerResult = _useIsolate ? await _checkerServiceIsolate.CheckAsync(checkerPath, inputFilePath, actualOutput, expectedOutputPath) : await _checkerService.CheckAsync(checkerPath, inputFilePath, actualOutput, expectedOutputPath); if (!checkerResult.Accepted) { _logger.LogWarning("Custom checker verdict: {Verdict} - {Message}", checkerResult.Verdict, checkerResult.Message); } return checkerResult.Accepted; } // Fall back to standard string comparison _logger.LogDebug("No custom checker, using standard comparison"); return await CheckOutputAsync(actualOutput, expectedOutputPath); } }