update polygon package parsing & testing
This commit is contained in:
@@ -6,10 +6,20 @@ 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)
|
||||
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)
|
||||
@@ -25,60 +35,17 @@ public class PackageParserService : IPackageParserService
|
||||
using var archive = new ZipArchive(packageStream, ZipArchiveMode.Read);
|
||||
archive.ExtractToDirectory(workingDirectory);
|
||||
|
||||
var package = new ProblemPackage
|
||||
// Check if this is a Polygon package (has problem.xml)
|
||||
var problemXmlPath = Path.Combine(workingDirectory, "problem.xml");
|
||||
if (File.Exists(problemXmlPath))
|
||||
{
|
||||
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;
|
||||
_logger.LogInformation("Detected Polygon package format (problem.xml found)");
|
||||
return await ParsePolygonPackageAsync(workingDirectory, problemXmlPath);
|
||||
}
|
||||
|
||||
// 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;
|
||||
// Fall back to legacy format (.in/.out files)
|
||||
_logger.LogInformation("Using legacy package format (.in/.out files)");
|
||||
return await ParseLegacyPackage(workingDirectory);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -87,6 +54,165 @@ public class PackageParserService : IPackageParserService
|
||||
}
|
||||
}
|
||||
|
||||
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)!;
|
||||
@@ -117,4 +243,68 @@ public class PackageParserService : IPackageParserService
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user