Some checks failed
Build and Push Docker Images / build (src/LiquidCode.Tester.Gateway/Dockerfile, git.nullptr.top/liquidcode/liquidcode-tester-gateway-roman, gateway) (push) Successful in 1m12s
Build and Push Docker Images / build (src/LiquidCode.Tester.Worker/Dockerfile, git.nullptr.top/liquidcode/liquidcode-tester-worker-roman, worker) (push) Has been cancelled
214 lines
7.4 KiB
C#
214 lines
7.4 KiB
C#
namespace LiquidCode.Tester.Worker.Services;
|
|
|
|
/// <summary>
|
|
/// Service for generating answer files by running the main solution from Polygon package
|
|
/// </summary>
|
|
public class AnswerGenerationService
|
|
{
|
|
private readonly ICompilationServiceFactory _compilationFactory;
|
|
private readonly IExecutionServiceFactory _executionFactory;
|
|
private readonly ILogger<AnswerGenerationService> _logger;
|
|
|
|
public AnswerGenerationService(
|
|
ICompilationServiceFactory compilationFactory,
|
|
IExecutionServiceFactory executionFactory,
|
|
ILogger<AnswerGenerationService> logger)
|
|
{
|
|
_compilationFactory = compilationFactory;
|
|
_executionFactory = executionFactory;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<bool> GenerateAnswersAsync(
|
|
PolygonProblemDescriptor descriptor,
|
|
string workingDirectory,
|
|
List<string> inputFilePaths,
|
|
List<string> answerFilePaths)
|
|
{
|
|
if (string.IsNullOrEmpty(descriptor.MainSolutionPath))
|
|
{
|
|
_logger.LogWarning("No main solution specified, cannot generate answers");
|
|
return false;
|
|
}
|
|
|
|
var solutionPath = Path.Combine(workingDirectory, descriptor.MainSolutionPath);
|
|
if (!File.Exists(solutionPath))
|
|
{
|
|
_logger.LogWarning("Main solution file not found: {Path}", solutionPath);
|
|
return false;
|
|
}
|
|
|
|
// Determine language and version from solution type
|
|
var (language, version) = ParseSolutionType(descriptor.MainSolutionType ?? "");
|
|
if (language == null)
|
|
{
|
|
_logger.LogWarning("Unsupported solution type: {Type}", descriptor.MainSolutionType);
|
|
return false;
|
|
}
|
|
|
|
_logger.LogInformation("Generating answers using {Language} {Version} solution: {Path}",
|
|
language, version, descriptor.MainSolutionPath);
|
|
|
|
try
|
|
{
|
|
// Read solution source code
|
|
var sourceCode = await File.ReadAllTextAsync(solutionPath);
|
|
|
|
// Get compilation service
|
|
var compilationService = _compilationFactory.GetCompilationService(language);
|
|
var executionService = _executionFactory.GetExecutionService(language);
|
|
|
|
// Compile solution
|
|
_logger.LogInformation("Compiling main solution...");
|
|
var compilationResult = await compilationService.CompileAsync(
|
|
sourceCode,
|
|
Path.GetDirectoryName(solutionPath)!,
|
|
version);
|
|
|
|
if (!compilationResult.Success)
|
|
{
|
|
_logger.LogError("Failed to compile main solution: {Error}", compilationResult.CompilerOutput);
|
|
return false;
|
|
}
|
|
|
|
_logger.LogInformation("Main solution compiled successfully");
|
|
|
|
// Generate answers for each test
|
|
int generatedCount = 0;
|
|
for (int i = 0; i < inputFilePaths.Count; i++)
|
|
{
|
|
var inputPath = inputFilePaths[i];
|
|
var answerPath = answerFilePaths[i];
|
|
|
|
if (!File.Exists(inputPath))
|
|
{
|
|
_logger.LogWarning("Input file not found: {Path}", inputPath);
|
|
continue;
|
|
}
|
|
|
|
_logger.LogDebug("Generating answer {Index}/{Total}: {AnswerPath}",
|
|
i + 1, inputFilePaths.Count, answerPath);
|
|
|
|
// Execute solution with input
|
|
var executionResult = await executionService.ExecuteAsync(
|
|
compilationResult.ExecutablePath!,
|
|
inputPath,
|
|
descriptor.TimeLimitMs * 2, // Give extra time for answer generation
|
|
descriptor.MemoryLimitMb * 2);
|
|
|
|
if (!executionResult.Success || executionResult.RuntimeError)
|
|
{
|
|
_logger.LogWarning("Failed to generate answer for {InputPath}: {Error}",
|
|
inputPath, executionResult.ErrorMessage);
|
|
continue;
|
|
}
|
|
|
|
// Save output as answer file
|
|
var answerDir = Path.GetDirectoryName(answerPath);
|
|
if (!string.IsNullOrEmpty(answerDir) && !Directory.Exists(answerDir))
|
|
{
|
|
Directory.CreateDirectory(answerDir);
|
|
}
|
|
|
|
await File.WriteAllTextAsync(answerPath, executionResult.Output);
|
|
generatedCount++;
|
|
|
|
_logger.LogDebug("Generated answer {Index}/{Total}", i + 1, inputFilePaths.Count);
|
|
}
|
|
|
|
_logger.LogInformation("Generated {Count} answer files out of {Total} tests",
|
|
generatedCount, inputFilePaths.Count);
|
|
|
|
return generatedCount > 0;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error generating answers");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private (string? language, string version) ParseSolutionType(string solutionType)
|
|
{
|
|
// Polygon solution types: python.2, python.3, cpp.g++17, cpp.g++20, java7, java8, etc.
|
|
if (string.IsNullOrEmpty(solutionType))
|
|
{
|
|
return (null, "");
|
|
}
|
|
|
|
if (solutionType.StartsWith("python"))
|
|
{
|
|
var versionPart = solutionType.Replace("python", string.Empty, StringComparison.OrdinalIgnoreCase)
|
|
.Trim('.', ' ');
|
|
|
|
if (string.IsNullOrWhiteSpace(versionPart))
|
|
{
|
|
return ("python", "3");
|
|
}
|
|
|
|
// Normalize python version; Polygon often uses python.3 or python3.10
|
|
versionPart = versionPart.TrimStart('.');
|
|
|
|
if (!versionPart.Contains('.'))
|
|
{
|
|
// Assume major version provided, default to CPython minor 10
|
|
versionPart = versionPart switch
|
|
{
|
|
"2" => "2.7",
|
|
"3" => "3.10",
|
|
_ => $"3.{versionPart}"
|
|
};
|
|
}
|
|
|
|
return ("python", versionPart);
|
|
}
|
|
|
|
if (solutionType.StartsWith("cpp."))
|
|
{
|
|
var standard = ExtractCppStandard(solutionType);
|
|
return ("cpp", standard);
|
|
}
|
|
|
|
if (solutionType.StartsWith("java"))
|
|
{
|
|
// java7, java8, java11
|
|
if (solutionType.Contains("11"))
|
|
return ("java", "11");
|
|
if (solutionType.Contains("8"))
|
|
return ("java", "8");
|
|
return ("java", "11"); // Default to Java 11
|
|
}
|
|
|
|
if (solutionType.StartsWith("csharp"))
|
|
{
|
|
return ("csharp", "9"); // Default to C# 9
|
|
}
|
|
|
|
if (solutionType.StartsWith("kotlin"))
|
|
{
|
|
return ("kotlin", "1.9"); // Default to Kotlin 1.9
|
|
}
|
|
|
|
_logger.LogWarning("Unknown solution type: {Type}", solutionType);
|
|
return (null, "");
|
|
}
|
|
|
|
private static string ExtractCppStandard(string solutionType)
|
|
{
|
|
var knownStandards = new[] { "26", "23", "20", "17", "14", "11", "03", "98" };
|
|
|
|
foreach (var standard in knownStandards)
|
|
{
|
|
if (solutionType.Contains($"++{standard}", StringComparison.OrdinalIgnoreCase) ||
|
|
solutionType.Contains($"c++{standard}", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// Normalize 03 to 03, 98 stays 98
|
|
return standard.TrimStart('0');
|
|
}
|
|
}
|
|
|
|
// Default to modern standard if not specified
|
|
return "17";
|
|
}
|
|
}
|