Files
LiquidCode.Tester/src/LiquidCode.Tester.Worker/Services/PackageParserService.cs
2025-10-28 22:01:35 +04:00

311 lines
11 KiB
C#

using System.IO.Compression;
using LiquidCode.Tester.Common.Models;
namespace LiquidCode.Tester.Worker.Services;
public class PackageParserService : IPackageParserService
{
private readonly ILogger<PackageParserService> _logger;
private readonly PolygonProblemXmlParser _polygonParser;
private readonly AnswerGenerationService _answerGenerator;
private readonly CppCompilationService _cppCompilation;
public PackageParserService(
ILogger<PackageParserService> logger,
PolygonProblemXmlParser polygonParser,
AnswerGenerationService answerGenerator,
CppCompilationService cppCompilation)
{
_logger = logger;
_polygonParser = polygonParser;
_answerGenerator = answerGenerator;
_cppCompilation = cppCompilation;
}
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);
// Check if this is a Polygon package (has problem.xml)
var problemXmlPath = Path.Combine(workingDirectory, "problem.xml");
if (File.Exists(problemXmlPath))
{
_logger.LogInformation("Detected Polygon package format (problem.xml found)");
return await ParsePolygonPackageAsync(workingDirectory, problemXmlPath);
}
// Fall back to legacy format (.in/.out files)
_logger.LogInformation("Using legacy package format (.in/.out files)");
return await ParseLegacyPackage(workingDirectory);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to parse package");
throw;
}
}
private async Task<ProblemPackage> ParsePolygonPackageAsync(string workingDirectory, string problemXmlPath)
{
var descriptor = _polygonParser.ParseProblemXml(problemXmlPath);
if (descriptor == null)
{
_logger.LogWarning("Failed to parse problem.xml, falling back to legacy format");
return await ParseLegacyPackage(workingDirectory);
}
var package = new ProblemPackage
{
WorkingDirectory = workingDirectory,
DefaultTimeLimit = descriptor.TimeLimitMs,
DefaultMemoryLimit = descriptor.MemoryLimitMb
};
// Collect test file paths and check which answers are missing
var inputPaths = new List<string>();
var answerPaths = new List<string>();
var missingAnswerPaths = new List<string>();
var missingAnswerInputs = new List<string>();
for (int i = 1; i <= descriptor.TestCount; i++)
{
var inputPath = Path.Combine(workingDirectory,
string.Format(descriptor.InputPathPattern.Replace("%02d", "{0:D2}"), i));
var answerPath = Path.Combine(workingDirectory,
string.Format(descriptor.AnswerPathPattern.Replace("%02d", "{0:D2}"), i));
if (!File.Exists(inputPath))
{
_logger.LogWarning("Input file not found: {InputPath}", inputPath);
continue;
}
inputPaths.Add(inputPath);
answerPaths.Add(answerPath);
if (!File.Exists(answerPath))
{
missingAnswerPaths.Add(answerPath);
missingAnswerInputs.Add(inputPath);
}
}
// Generate missing answer files if we have a main solution
if (missingAnswerPaths.Count > 0)
{
_logger.LogInformation("Found {Count} tests without answer files, attempting to generate them",
missingAnswerPaths.Count);
var generated = await _answerGenerator.GenerateAnswersAsync(
descriptor,
workingDirectory,
missingAnswerInputs,
missingAnswerPaths);
if (generated)
{
_logger.LogInformation("Successfully generated answer files");
}
else
{
_logger.LogWarning("Failed to generate answer files, tests without answers will be skipped");
}
}
// Now create test cases for all tests that have answer files
for (int i = 0; i < inputPaths.Count; i++)
{
var inputPath = inputPaths[i];
var answerPath = answerPaths[i];
if (!File.Exists(answerPath))
{
_logger.LogWarning("Answer file not found: {AnswerPath} (skipping test)", answerPath);
continue;
}
package.TestCases.Add(new TestCase
{
Number = i + 1,
InputFilePath = inputPath,
OutputFilePath = answerPath,
TimeLimit = descriptor.TimeLimitMs,
MemoryLimit = descriptor.MemoryLimitMb
});
}
// Look for and compile checker
package.CheckerPath = await FindAndCompileCheckerAsync(workingDirectory);
if (package.TestCases.Count == 0)
{
_logger.LogWarning("No test cases with answer files found! Expected format: {InputPattern} -> {AnswerPattern}",
descriptor.InputPathPattern, descriptor.AnswerPathPattern);
}
_logger.LogInformation("Parsed Polygon package with {TestCount} tests (out of {TotalTests} in problem.xml)",
package.TestCases.Count, descriptor.TestCount);
return package;
}
private async Task<ProblemPackage> ParseLegacyPackage(string 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 and compile checker
package.CheckerPath = await FindAndCompileCheckerAsync(workingDirectory);
if (package.TestCases.Count == 0)
{
_logger.LogWarning("No test cases found! Check package structure. Expected .in/.out files in tests directory or root");
}
_logger.LogInformation("Parsed legacy package with {TestCount} tests", package.TestCases.Count);
return package;
}
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;
}
private async Task<string?> FindAndCompileCheckerAsync(string workingDirectory)
{
// Try to find checker in common locations
var checkerCandidates = new[]
{
Path.Combine(workingDirectory, "check.exe"),
Path.Combine(workingDirectory, "checker.exe"),
Path.Combine(workingDirectory, "check.cpp"),
Path.Combine(workingDirectory, "checker.cpp"),
Path.Combine(workingDirectory, "files", "check.exe"),
Path.Combine(workingDirectory, "files", "checker.exe"),
Path.Combine(workingDirectory, "files", "check.cpp"),
Path.Combine(workingDirectory, "files", "checker.cpp")
};
foreach (var candidate in checkerCandidates)
{
if (!File.Exists(candidate))
continue;
// If it's already an executable, use it
if (candidate.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
{
_logger.LogInformation("Found checker executable: {CheckerPath}", candidate);
return candidate;
}
// If it's C++ source, compile it
if (candidate.EndsWith(".cpp", StringComparison.OrdinalIgnoreCase))
{
_logger.LogInformation("Found checker source: {CheckerPath}, compiling...", candidate);
try
{
var checkerSource = await File.ReadAllTextAsync(candidate);
var checkerDir = Path.GetDirectoryName(candidate)!;
// Compile checker with C++17 (testlib.h compatible)
var compilationResult = await _cppCompilation.CompileAsync(
checkerSource,
checkerDir,
"17");
if (!compilationResult.Success)
{
_logger.LogError("Failed to compile checker: {Error}", compilationResult.CompilerOutput);
continue; // Try next candidate
}
_logger.LogInformation("Checker compiled successfully: {ExecutablePath}", compilationResult.ExecutablePath);
return compilationResult.ExecutablePath;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error compiling checker: {CheckerPath}", candidate);
continue; // Try next candidate
}
}
}
_logger.LogWarning("No checker found in package");
return null;
}
}