add compile & test worker

This commit is contained in:
prixod
2025-10-24 23:46:51 +04:00
parent 3d854c3470
commit 6cead15a5f
19 changed files with 849 additions and 13 deletions

View File

@@ -12,14 +12,7 @@
</component>
<component name="ChangeListManager">
<list default="true" id="1d3190f0-8175-44b9-bab6-12e025e4819d" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/global.json" beforeDir="false" afterPath="$PROJECT_DIR$/global.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/LiquidCode.Tester.Common/LiquidCode.Tester.Common.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/src/LiquidCode.Tester.Common/LiquidCode.Tester.Common.csproj" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/LiquidCode.Tester.Common/Program.cs" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/LiquidCode.Tester.Gateway/LiquidCode.Tester.Gateway.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/src/LiquidCode.Tester.Gateway/LiquidCode.Tester.Gateway.csproj" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/LiquidCode.Tester.Gateway/Program.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/LiquidCode.Tester.Gateway/Program.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/LiquidCode.Tester.Gateway/appsettings.Development.json" beforeDir="false" afterPath="$PROJECT_DIR$/src/LiquidCode.Tester.Gateway/appsettings.Development.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/LiquidCode.Tester.Gateway/appsettings.json" beforeDir="false" afterPath="$PROJECT_DIR$/src/LiquidCode.Tester.Gateway/appsettings.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/LiquidCode.Tester.Worker/LiquidCode.Tester.Worker.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/src/LiquidCode.Tester.Worker/LiquidCode.Tester.Worker.csproj" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/LiquidCode.Tester.Worker/Program.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/LiquidCode.Tester.Worker/Program.cs" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -32,6 +25,23 @@
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="HighlightingSettingsPerFile">
<setting file="mock://C:/Users/prixod/source/repos/LiquidCode.Tester/global.json" root0="FORCE_HIGHLIGHTING" />
<setting file="mock://C:/Users/prixod/source/repos/LiquidCode.Tester/global.json" root0="FORCE_HIGHLIGHTING" />
<setting file="mock://C:/Users/prixod/source/repos/LiquidCode.Tester/src/LiquidCode.Tester.Common/Models/ErrorCode.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="mock://C:/Users/prixod/source/repos/LiquidCode.Tester/src/LiquidCode.Tester.Gateway/Controllers/TesterController.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="mock://C:/Users/prixod/source/repos/LiquidCode.Tester/src/LiquidCode.Tester.Gateway/Program.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="mock://C:/Users/prixod/source/repos/LiquidCode.Tester/src/LiquidCode.Tester.Gateway/Program.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="mock://C:/Users/prixod/source/repos/LiquidCode.Tester/src/LiquidCode.Tester.Gateway/Services/IPackageDownloadService.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="mock://C:/Users/prixod/source/repos/LiquidCode.Tester/src/LiquidCode.Tester.Gateway/Services/IWorkerClientService.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="mock://C:/Users/prixod/source/repos/LiquidCode.Tester/src/LiquidCode.Tester.Gateway/Services/PackageDownloadService.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="mock://C:/Users/prixod/source/repos/LiquidCode.Tester/src/LiquidCode.Tester.Gateway/Services/WorkerClientService.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="mock://C:/Users/prixod/source/repos/LiquidCode.Tester/src/LiquidCode.Tester.Gateway/appsettings.Development.json" root0="FORCE_HIGHLIGHTING" />
<setting file="mock://C:/Users/prixod/source/repos/LiquidCode.Tester/src/LiquidCode.Tester.Gateway/appsettings.Development.json" root0="FORCE_HIGHLIGHTING" />
<setting file="mock://C:/Users/prixod/source/repos/LiquidCode.Tester/src/LiquidCode.Tester.Gateway/appsettings.Development.json" root0="FORCE_HIGHLIGHTING" />
<setting file="mock://C:/Users/prixod/source/repos/LiquidCode.Tester/src/LiquidCode.Tester.Gateway/appsettings.json" root0="FORCE_HIGHLIGHTING" />
<setting file="mock://C:/Users/prixod/source/repos/LiquidCode.Tester/src/LiquidCode.Tester.Gateway/appsettings.json" root0="FORCE_HIGHLIGHTING" />
</component>
<component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" />
<component name="ProjectColorInfo"><![CDATA[{
"associatedIndex": 1
@@ -265,7 +275,7 @@
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1761331001679</updated>
<workItem from="1761331002792" duration="2278000" />
<workItem from="1761331002792" duration="3067000" />
</task>
<task id="LOCAL-00001" summary="init">
<option name="closed" value="true" />
@@ -275,7 +285,15 @@
<option name="project" value="LOCAL" />
<updated>1761333540672</updated>
</task>
<option name="localTasksCounter" value="2" />
<task id="LOCAL-00002" summary="update">
<option name="closed" value="true" />
<created>1761334197106</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1761334197106</updated>
</task>
<option name="localTasksCounter" value="3" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -297,7 +315,8 @@
<component name="VcsManagerConfiguration">
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
<MESSAGE value="init" />
<option name="LAST_COMMIT_MESSAGE" value="init" />
<MESSAGE value="update" />
<option name="LAST_COMMIT_MESSAGE" value="update" />
</component>
<component name="XDebuggerManager">
<breakpoint-manager>

View File

@@ -0,0 +1,10 @@
namespace LiquidCode.Tester.Common.Models;
public class ProblemPackage
{
public string WorkingDirectory { get; set; } = string.Empty;
public List<TestCase> TestCases { get; set; } = new();
public string? CheckerPath { get; set; }
public int DefaultTimeLimit { get; set; } = 2000; // milliseconds
public int DefaultMemoryLimit { get; set; } = 256; // MB
}

View File

@@ -0,0 +1,10 @@
namespace LiquidCode.Tester.Common.Models;
public class TestCase
{
public int Number { get; set; }
public string InputFilePath { get; set; } = string.Empty;
public string OutputFilePath { get; set; } = string.Empty;
public int TimeLimit { get; set; } // milliseconds
public int MemoryLimit { get; set; } // MB
}

View File

@@ -0,0 +1,64 @@
using LiquidCode.Tester.Worker.Services;
using Microsoft.AspNetCore.Mvc;
namespace LiquidCode.Tester.Worker.Controllers;
[ApiController]
[Route("api/[controller]")]
public class TestController : ControllerBase
{
private readonly ITestingService _testingService;
private readonly ILogger<TestController> _logger;
public TestController(ITestingService testingService, ILogger<TestController> logger)
{
_testingService = testingService;
_logger = logger;
}
[HttpPost]
public async Task<IActionResult> Test([FromForm] TestRequest request)
{
_logger.LogInformation("Received test request for submit {SubmitId}", request.Id);
try
{
// Start testing in background
_ = Task.Run(async () =>
{
try
{
await _testingService.ProcessSubmitAsync(request);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing submit {SubmitId}", request.Id);
}
});
return Accepted(new { message = "Test request accepted", submitId = request.Id });
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to accept test request for submit {SubmitId}", request.Id);
return StatusCode(500, new { error = "Failed to accept test request", details = ex.Message });
}
}
[HttpGet("health")]
public IActionResult Health()
{
return Ok(new { status = "healthy", service = "cpp-worker", timestamp = DateTime.UtcNow });
}
}
public class TestRequest
{
public long Id { get; set; }
public long MissionId { get; set; }
public string Language { get; set; } = string.Empty;
public string LanguageVersion { get; set; } = string.Empty;
public string SourceCode { get; set; } = string.Empty;
public string CallbackUrl { get; set; } = string.Empty;
public IFormFile? Package { get; set; }
}

View File

@@ -1,3 +1,30 @@
// See https://aka.ms/new-console-template for more information
using LiquidCode.Tester.Worker.Services;
Console.WriteLine("Hello, World!");
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddControllers();
builder.Services.AddOpenApi();
// Add HttpClient
builder.Services.AddHttpClient();
// Register application services
builder.Services.AddSingleton<IPackageParserService, PackageParserService>();
builder.Services.AddSingleton<ICompilationService, CppCompilationService>();
builder.Services.AddSingleton<IExecutionService, CppExecutionService>();
builder.Services.AddSingleton<IOutputCheckerService, OutputCheckerService>();
builder.Services.AddSingleton<ICallbackService, CallbackService>();
builder.Services.AddSingleton<ITestingService, TestingService>();
var app = builder.Build();
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.MapControllers();
app.Run();

View File

@@ -0,0 +1,39 @@
using System.Text;
using System.Text.Json;
using LiquidCode.Tester.Common.Models;
namespace LiquidCode.Tester.Worker.Services;
public class CallbackService : ICallbackService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<CallbackService> _logger;
public CallbackService(IHttpClientFactory httpClientFactory, ILogger<CallbackService> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}
public async Task SendStatusAsync(string callbackUrl, TesterResponseModel response)
{
_logger.LogInformation("Sending status update to {CallbackUrl} for submit {SubmitId}", callbackUrl, response.SubmitId);
try
{
var httpClient = _httpClientFactory.CreateClient();
var json = JsonSerializer.Serialize(response);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var httpResponse = await httpClient.PostAsync(callbackUrl, content);
httpResponse.EnsureSuccessStatusCode();
_logger.LogInformation("Status update sent successfully");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send status update to {CallbackUrl}", callbackUrl);
// Don't throw - callback failures shouldn't stop testing
}
}
}

View File

@@ -0,0 +1,86 @@
using System.Diagnostics;
namespace LiquidCode.Tester.Worker.Services;
public class CppCompilationService : ICompilationService
{
private readonly ILogger<CppCompilationService> _logger;
private readonly IConfiguration _configuration;
public CppCompilationService(ILogger<CppCompilationService> logger, IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
}
public async Task<CompilationResult> CompileAsync(string sourceCode, string workingDirectory)
{
var sourceFilePath = Path.Combine(workingDirectory, "solution.cpp");
var executablePath = Path.Combine(workingDirectory, "solution");
_logger.LogInformation("Compiling C++ code in {WorkingDirectory}", workingDirectory);
try
{
// Write source code to file
await File.WriteAllTextAsync(sourceFilePath, sourceCode);
// Compile using g++
var compiler = _configuration["Cpp:Compiler"] ?? "g++";
var compilerFlags = _configuration["Cpp:CompilerFlags"] ?? "-O2 -std=c++17 -Wall";
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = compiler,
Arguments = $"{compilerFlags} {sourceFilePath} -o {executablePath}",
WorkingDirectory = workingDirectory,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
var output = await process.StandardOutput.ReadToEndAsync();
var error = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
var compilerOutput = $"{output}\n{error}".Trim();
if (process.ExitCode == 0 && File.Exists(executablePath))
{
_logger.LogInformation("Compilation successful");
return new CompilationResult
{
Success = true,
ExecutablePath = executablePath,
CompilerOutput = compilerOutput
};
}
else
{
_logger.LogWarning("Compilation failed with exit code {ExitCode}", process.ExitCode);
return new CompilationResult
{
Success = false,
ErrorMessage = "Compilation failed",
CompilerOutput = compilerOutput
};
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during compilation");
return new CompilationResult
{
Success = false,
ErrorMessage = $"Compilation error: {ex.Message}"
};
}
}
}

View File

@@ -0,0 +1,114 @@
using System.Diagnostics;
namespace LiquidCode.Tester.Worker.Services;
public class CppExecutionService : IExecutionService
{
private readonly ILogger<CppExecutionService> _logger;
public CppExecutionService(ILogger<CppExecutionService> logger)
{
_logger = logger;
}
public async Task<ExecutionResult> ExecuteAsync(string executablePath, string inputFilePath, int timeLimitMs, int memoryLimitMb)
{
_logger.LogInformation("Executing {Executable} with input {Input}, time limit {TimeLimit}ms, memory limit {MemoryLimit}MB",
executablePath, inputFilePath, timeLimitMs, memoryLimitMb);
var result = new ExecutionResult();
var stopwatch = Stopwatch.StartNew();
try
{
using var inputStream = File.OpenRead(inputFilePath);
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = executablePath,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
// Copy input to stdin
await inputStream.CopyToAsync(process.StandardInput.BaseStream);
process.StandardInput.Close();
// Read output
var outputTask = process.StandardOutput.ReadToEndAsync();
var errorTask = process.StandardError.ReadToEndAsync();
// Wait for completion with timeout
var completedInTime = await Task.Run(() => process.WaitForExit(timeLimitMs));
stopwatch.Stop();
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
if (!completedInTime)
{
// Time limit exceeded
try
{
process.Kill(entireProcessTree: true);
}
catch { }
result.TimeLimitExceeded = true;
result.ErrorMessage = "Time limit exceeded";
_logger.LogWarning("Execution exceeded time limit");
return result;
}
result.Output = await outputTask;
result.ErrorOutput = await errorTask;
result.ExitCode = process.ExitCode;
// Check for runtime error
if (process.ExitCode != 0)
{
result.RuntimeError = true;
result.ErrorMessage = $"Runtime error (exit code {process.ExitCode})";
_logger.LogWarning("Runtime error with exit code {ExitCode}", process.ExitCode);
return result;
}
// Estimate memory usage (basic approach - in real scenario we'd use cgroups)
try
{
result.MemoryUsedMb = process.PeakWorkingSet64 / (1024 * 1024);
if (result.MemoryUsedMb > memoryLimitMb)
{
result.MemoryLimitExceeded = true;
result.ErrorMessage = "Memory limit exceeded";
_logger.LogWarning("Memory limit exceeded: {Used}MB > {Limit}MB", result.MemoryUsedMb, memoryLimitMb);
return result;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not measure memory usage");
}
result.Success = true;
_logger.LogInformation("Execution completed successfully in {Time}ms", result.ExecutionTimeMs);
}
catch (Exception ex)
{
stopwatch.Stop();
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
result.RuntimeError = true;
result.ErrorMessage = $"Execution error: {ex.Message}";
_logger.LogError(ex, "Error during execution");
}
return result;
}
}

View File

@@ -0,0 +1,11 @@
using LiquidCode.Tester.Common.Models;
namespace LiquidCode.Tester.Worker.Services;
public interface ICallbackService
{
/// <summary>
/// Sends a status update to the callback URL
/// </summary>
Task SendStatusAsync(string callbackUrl, TesterResponseModel response);
}

View File

@@ -0,0 +1,20 @@
namespace LiquidCode.Tester.Worker.Services;
public interface ICompilationService
{
/// <summary>
/// Compiles source code to an executable
/// </summary>
/// <param name="sourceCode">Source code to compile</param>
/// <param name="workingDirectory">Directory to compile in</param>
/// <returns>Result containing success status, executable path, and error messages</returns>
Task<CompilationResult> CompileAsync(string sourceCode, string workingDirectory);
}
public class CompilationResult
{
public bool Success { get; set; }
public string? ExecutablePath { get; set; }
public string ErrorMessage { get; set; } = string.Empty;
public string CompilerOutput { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,28 @@
namespace LiquidCode.Tester.Worker.Services;
public interface IExecutionService
{
/// <summary>
/// Executes a compiled program with input and captures output
/// </summary>
/// <param name="executablePath">Path to the executable</param>
/// <param name="inputFilePath">Path to the input file</param>
/// <param name="timeLimitMs">Time limit in milliseconds</param>
/// <param name="memoryLimitMb">Memory limit in megabytes</param>
/// <returns>Execution result</returns>
Task<ExecutionResult> ExecuteAsync(string executablePath, string inputFilePath, int timeLimitMs, int memoryLimitMb);
}
public class ExecutionResult
{
public bool Success { get; set; }
public string Output { get; set; } = string.Empty;
public string ErrorOutput { get; set; } = string.Empty;
public int ExitCode { get; set; }
public long ExecutionTimeMs { get; set; }
public long MemoryUsedMb { get; set; }
public bool TimeLimitExceeded { get; set; }
public bool MemoryLimitExceeded { get; set; }
public bool RuntimeError { get; set; }
public string ErrorMessage { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,12 @@
namespace LiquidCode.Tester.Worker.Services;
public interface IOutputCheckerService
{
/// <summary>
/// Compares actual output with expected output
/// </summary>
/// <param name="actualOutput">Output from user's solution</param>
/// <param name="expectedOutputPath">Path to expected output file</param>
/// <returns>True if outputs match</returns>
Task<bool> CheckOutputAsync(string actualOutput, string expectedOutputPath);
}

View File

@@ -0,0 +1,13 @@
using LiquidCode.Tester.Common.Models;
namespace LiquidCode.Tester.Worker.Services;
public interface IPackageParserService
{
/// <summary>
/// Extracts and parses a Polygon problem package
/// </summary>
/// <param name="packageStream">Stream containing the package ZIP file</param>
/// <returns>Parsed problem package information</returns>
Task<ProblemPackage> ParsePackageAsync(Stream packageStream);
}

View File

@@ -0,0 +1,11 @@
using LiquidCode.Tester.Worker.Controllers;
namespace LiquidCode.Tester.Worker.Services;
public interface ITestingService
{
/// <summary>
/// Processes a complete submit: parse package, compile, test, send callbacks
/// </summary>
Task ProcessSubmitAsync(TestRequest request);
}

View File

@@ -0,0 +1,54 @@
namespace LiquidCode.Tester.Worker.Services;
public class OutputCheckerService : IOutputCheckerService
{
private readonly ILogger<OutputCheckerService> _logger;
public OutputCheckerService(ILogger<OutputCheckerService> logger)
{
_logger = logger;
}
public async Task<bool> 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);
}
return string.Join("\n", lines);
}
}

View File

@@ -0,0 +1,120 @@
using System.IO.Compression;
using LiquidCode.Tester.Common.Models;
namespace LiquidCode.Tester.Worker.Services;
public class PackageParserService : IPackageParserService
{
private readonly ILogger<PackageParserService> _logger;
public PackageParserService(ILogger<PackageParserService> logger)
{
_logger = logger;
}
public async Task<ProblemPackage> ParsePackageAsync(Stream packageStream)
{
var workingDirectory = Path.Combine(Path.GetTempPath(), $"problem_{Guid.NewGuid()}");
Directory.CreateDirectory(workingDirectory);
_logger.LogInformation("Extracting package to {WorkingDirectory}", workingDirectory);
try
{
// Extract ZIP archive
using var archive = new ZipArchive(packageStream, ZipArchiveMode.Read);
archive.ExtractToDirectory(workingDirectory);
var package = new ProblemPackage
{
WorkingDirectory = workingDirectory
};
// Find tests directory
var testsDir = Path.Combine(workingDirectory, "tests");
if (!Directory.Exists(testsDir))
{
_logger.LogWarning("Tests directory not found, searching for test files in root");
testsDir = workingDirectory;
}
// Parse test cases
var inputFiles = Directory.GetFiles(testsDir, "*", SearchOption.AllDirectories)
.Where(f => Path.GetFileName(f).EndsWith(".in") || Path.GetFileName(f).Contains("input"))
.OrderBy(f => f)
.ToList();
for (int i = 0; i < inputFiles.Count; i++)
{
var inputFile = inputFiles[i];
var outputFile = FindCorrespondingOutputFile(inputFile);
if (outputFile == null)
{
_logger.LogWarning("No output file found for input {InputFile}", inputFile);
continue;
}
package.TestCases.Add(new TestCase
{
Number = i + 1,
InputFilePath = inputFile,
OutputFilePath = outputFile,
TimeLimit = package.DefaultTimeLimit,
MemoryLimit = package.DefaultMemoryLimit
});
}
// Look for checker
var checkerCandidates = new[] { "check.cpp", "checker.cpp", "check", "checker" };
foreach (var candidate in checkerCandidates)
{
var checkerPath = Path.Combine(workingDirectory, candidate);
if (File.Exists(checkerPath))
{
package.CheckerPath = checkerPath;
break;
}
}
_logger.LogInformation("Parsed package with {TestCount} tests", package.TestCases.Count);
return package;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to parse package");
throw;
}
}
private string? FindCorrespondingOutputFile(string inputFile)
{
var directory = Path.GetDirectoryName(inputFile)!;
var fileName = Path.GetFileNameWithoutExtension(inputFile);
var extension = Path.GetExtension(inputFile);
// Try various output file naming patterns
var patterns = new[]
{
fileName.Replace("input", "output") + ".out",
fileName.Replace("input", "output") + ".a",
fileName.Replace("input", "answer") + ".out",
fileName.Replace("input", "answer") + ".a",
fileName + ".out",
fileName + ".a",
fileName.Replace(".in", ".out"),
fileName.Replace(".in", ".a")
};
foreach (var pattern in patterns)
{
var candidate = Path.Combine(directory, pattern);
if (File.Exists(candidate))
{
return candidate;
}
}
return null;
}
}

View File

@@ -0,0 +1,177 @@
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<TestingService> _logger;
public TestingService(
IPackageParserService packageParser,
ICompilationService compilationService,
IExecutionService executionService,
IOutputCheckerService outputChecker,
ICallbackService callbackService,
ILogger<TestingService> 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);
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore": "Information"
}
}
}

View File

@@ -0,0 +1,13 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Cpp": {
"Compiler": "g++",
"CompilerFlags": "-O2 -std=c++17 -Wall"
}
}