311 lines
11 KiB
C#
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;
|
|
}
|
|
}
|