using LiquidCode.Tester.Common.Models; using LiquidCode.Tester.Worker.Controllers; namespace LiquidCode.Tester.Worker.Services; public class TestingService : ITestingService { private readonly IPackageParserService _packageParser; private readonly ICompilationService _compilationService; private readonly IExecutionService _executionService; private readonly IOutputCheckerService _outputChecker; private readonly ICallbackService _callbackService; private readonly ILogger _logger; public TestingService( IPackageParserService packageParser, ICompilationService compilationService, IExecutionService executionService, IOutputCheckerService outputChecker, ICallbackService callbackService, ILogger logger) { _packageParser = packageParser; _compilationService = compilationService; _executionService = executionService; _outputChecker = outputChecker; _callbackService = callbackService; _logger = logger; } public async Task ProcessSubmitAsync(TestRequest request) { _logger.LogInformation("Starting to process submit {SubmitId}", request.Id); try { // Send initial status await SendStatusAsync(request, State.Waiting, ErrorCode.None, "Submit received", 0, 0); // Parse package ProblemPackage package; if (request.Package != null) { using var packageStream = request.Package.OpenReadStream(); package = await _packageParser.ParsePackageAsync(packageStream); } else { throw new InvalidOperationException("No package provided"); } _logger.LogInformation("Package parsed, found {TestCount} tests", package.TestCases.Count); // Send compiling status await SendStatusAsync(request, State.Compiling, ErrorCode.None, "Compiling solution", 0, package.TestCases.Count); // Compile user solution var compilationResult = await _compilationService.CompileAsync(request.SourceCode, package.WorkingDirectory); if (!compilationResult.Success) { _logger.LogWarning("Compilation failed for submit {SubmitId}", request.Id); await SendStatusAsync(request, State.Done, ErrorCode.CompileError, $"Compilation failed: {compilationResult.CompilerOutput}", 0, package.TestCases.Count); return; } _logger.LogInformation("Compilation successful"); // Send testing status await SendStatusAsync(request, State.Testing, ErrorCode.None, "Running tests", 0, package.TestCases.Count); // Run tests for (int i = 0; i < package.TestCases.Count; i++) { var testCase = package.TestCases[i]; _logger.LogInformation("Running test {TestNumber}/{TotalTests}", testCase.Number, package.TestCases.Count); await SendStatusAsync(request, State.Testing, ErrorCode.None, $"Running test {testCase.Number}", testCase.Number, package.TestCases.Count); // Execute solution var executionResult = await _executionService.ExecuteAsync( compilationResult.ExecutablePath!, testCase.InputFilePath, testCase.TimeLimit, testCase.MemoryLimit); // Check for execution errors if (executionResult.TimeLimitExceeded) { _logger.LogWarning("Time limit exceeded on test {TestNumber}", testCase.Number); await SendStatusAsync(request, State.Done, ErrorCode.TimeLimitError, $"Time limit exceeded on test {testCase.Number}", testCase.Number, package.TestCases.Count); CleanupWorkingDirectory(package.WorkingDirectory); return; } if (executionResult.MemoryLimitExceeded) { _logger.LogWarning("Memory limit exceeded on test {TestNumber}", testCase.Number); await SendStatusAsync(request, State.Done, ErrorCode.MemoryError, $"Memory limit exceeded on test {testCase.Number}", testCase.Number, package.TestCases.Count); CleanupWorkingDirectory(package.WorkingDirectory); return; } if (executionResult.RuntimeError) { _logger.LogWarning("Runtime error on test {TestNumber}: {Error}", testCase.Number, executionResult.ErrorMessage); await SendStatusAsync(request, State.Done, ErrorCode.RuntimeError, $"Runtime error on test {testCase.Number}: {executionResult.ErrorMessage}", testCase.Number, package.TestCases.Count); CleanupWorkingDirectory(package.WorkingDirectory); return; } // Check output var outputCorrect = await _outputChecker.CheckOutputAsync(executionResult.Output, testCase.OutputFilePath); if (!outputCorrect) { _logger.LogWarning("Wrong answer on test {TestNumber}", testCase.Number); await SendStatusAsync(request, State.Done, ErrorCode.IncorrectAnswer, $"Wrong answer on test {testCase.Number}", testCase.Number, package.TestCases.Count); CleanupWorkingDirectory(package.WorkingDirectory); return; } _logger.LogInformation("Test {TestNumber} passed", testCase.Number); } // All tests passed! _logger.LogInformation("All tests passed for submit {SubmitId}", request.Id); await SendStatusAsync(request, State.Done, ErrorCode.None, "All tests passed", package.TestCases.Count, package.TestCases.Count); // Cleanup CleanupWorkingDirectory(package.WorkingDirectory); } catch (Exception ex) { _logger.LogError(ex, "Error processing submit {SubmitId}", request.Id); await SendStatusAsync(request, State.Done, ErrorCode.UnknownError, $"Internal error: {ex.Message}", 0, 0); } } private async Task SendStatusAsync(TestRequest request, State state, ErrorCode errorCode, string message, int currentTest, int totalTests) { var response = new TesterResponseModel( SubmitId: request.Id, State: state, ErrorCode: errorCode, Message: message, CurrentTest: currentTest, AmountOfTests: totalTests ); await _callbackService.SendStatusAsync(request.CallbackUrl, response); } private void CleanupWorkingDirectory(string workingDirectory) { try { if (Directory.Exists(workingDirectory)) { Directory.Delete(workingDirectory, recursive: true); _logger.LogInformation("Cleaned up working directory {WorkingDirectory}", workingDirectory); } } catch (Exception ex) { _logger.LogWarning(ex, "Failed to cleanup working directory {WorkingDirectory}", workingDirectory); } } }