add compile & test worker
This commit is contained in:
41
.idea/.idea.LiquidCode.Tester/.idea/workspace.xml
generated
41
.idea/.idea.LiquidCode.Tester/.idea/workspace.xml
generated
@@ -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>
|
||||
|
||||
10
src/LiquidCode.Tester.Common/Models/ProblemPackage.cs
Normal file
10
src/LiquidCode.Tester.Common/Models/ProblemPackage.cs
Normal 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
|
||||
}
|
||||
10
src/LiquidCode.Tester.Common/Models/TestCase.cs
Normal file
10
src/LiquidCode.Tester.Common/Models/TestCase.cs
Normal 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
|
||||
}
|
||||
64
src/LiquidCode.Tester.Worker/Controllers/TestController.cs
Normal file
64
src/LiquidCode.Tester.Worker/Controllers/TestController.cs
Normal 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; }
|
||||
}
|
||||
@@ -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();
|
||||
39
src/LiquidCode.Tester.Worker/Services/CallbackService.cs
Normal file
39
src/LiquidCode.Tester.Worker/Services/CallbackService.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
114
src/LiquidCode.Tester.Worker/Services/CppExecutionService.cs
Normal file
114
src/LiquidCode.Tester.Worker/Services/CppExecutionService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
11
src/LiquidCode.Tester.Worker/Services/ICallbackService.cs
Normal file
11
src/LiquidCode.Tester.Worker/Services/ICallbackService.cs
Normal 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);
|
||||
}
|
||||
20
src/LiquidCode.Tester.Worker/Services/ICompilationService.cs
Normal file
20
src/LiquidCode.Tester.Worker/Services/ICompilationService.cs
Normal 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;
|
||||
}
|
||||
28
src/LiquidCode.Tester.Worker/Services/IExecutionService.cs
Normal file
28
src/LiquidCode.Tester.Worker/Services/IExecutionService.cs
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
11
src/LiquidCode.Tester.Worker/Services/ITestingService.cs
Normal file
11
src/LiquidCode.Tester.Worker/Services/ITestingService.cs
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
120
src/LiquidCode.Tester.Worker/Services/PackageParserService.cs
Normal file
120
src/LiquidCode.Tester.Worker/Services/PackageParserService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
177
src/LiquidCode.Tester.Worker/Services/TestingService.cs
Normal file
177
src/LiquidCode.Tester.Worker/Services/TestingService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"Microsoft.AspNetCore": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/LiquidCode.Tester.Worker/appsettings.json
Normal file
13
src/LiquidCode.Tester.Worker/appsettings.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Cpp": {
|
||||
"Compiler": "g++",
|
||||
"CompilerFlags": "-O2 -std=c++17 -Wall"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user