update polygon package parsing & testing

This commit is contained in:
prixod
2025-10-28 21:10:39 +04:00
parent 6041acb8ed
commit 69829569bb
13 changed files with 1822 additions and 57 deletions

View File

@@ -10,6 +10,9 @@ builder.Services.AddOpenApi();
builder.Services.AddHttpClient();
// Register application services
builder.Services.AddSingleton<PolygonProblemXmlParser>();
builder.Services.AddSingleton<AnswerGenerationService>();
builder.Services.AddSingleton<CheckerService>();
builder.Services.AddSingleton<IPackageParserService, PackageParserService>();
builder.Services.AddSingleton<IOutputCheckerService, OutputCheckerService>();
builder.Services.AddSingleton<ICallbackService, CallbackService>();

View File

@@ -0,0 +1,181 @@
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 parts = solutionType.Split('.');
var version = parts.Length > 1 ? parts[1] : "3";
return ("python", $"3.{version}"); // Map python.3 -> 3.3, python.2 -> 3.2 (approx)
}
if (solutionType.StartsWith("cpp."))
{
// cpp.g++17, cpp.g++20, cpp.g++14
if (solutionType.Contains("++20"))
return ("cpp", "20");
if (solutionType.Contains("++17"))
return ("cpp", "17");
if (solutionType.Contains("++14"))
return ("cpp", "14");
return ("cpp", "17"); // Default to C++17
}
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, "");
}
}

View File

@@ -0,0 +1,165 @@
using System.Diagnostics;
namespace LiquidCode.Tester.Worker.Services;
/// <summary>
/// Service for running custom checkers (testlib-based)
/// </summary>
public class CheckerService
{
private readonly ILogger<CheckerService> _logger;
public CheckerService(ILogger<CheckerService> logger)
{
_logger = logger;
}
/// <summary>
/// Check user output using custom checker
/// </summary>
/// <param name="checkerPath">Path to checker executable</param>
/// <param name="inputPath">Path to input file</param>
/// <param name="userOutput">User program output</param>
/// <param name="answerPath">Path to answer file</param>
/// <returns>Checker result</returns>
public async Task<CheckerResult> CheckAsync(
string checkerPath,
string inputPath,
string userOutput,
string answerPath)
{
if (!File.Exists(checkerPath))
{
_logger.LogError("Checker not found: {CheckerPath}", checkerPath);
return new CheckerResult
{
Accepted = false,
ExitCode = -1,
Message = "Checker executable not found"
};
}
// Save user output to temporary file
var tempOutputPath = Path.Combine(Path.GetTempPath(), $"user_output_{Guid.NewGuid()}.txt");
try
{
await File.WriteAllTextAsync(tempOutputPath, userOutput);
_logger.LogDebug("Running checker: {Checker} {Input} {Output} {Answer}",
checkerPath, inputPath, tempOutputPath, answerPath);
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = checkerPath,
Arguments = $"\"{inputPath}\" \"{tempOutputPath}\" \"{answerPath}\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
var stdout = await process.StandardOutput.ReadToEndAsync();
var stderr = await process.StandardError.ReadToEndAsync();
// Wait with timeout (checkers should be fast)
var completed = await Task.Run(() => process.WaitForExit(5000));
if (!completed)
{
_logger.LogWarning("Checker timeout, killing process");
try
{
process.Kill(entireProcessTree: true);
}
catch { }
return new CheckerResult
{
Accepted = false,
ExitCode = -1,
Message = "Checker timeout"
};
}
var exitCode = process.ExitCode;
_logger.LogDebug("Checker exit code: {ExitCode}, stderr: {Stderr}", exitCode, stderr);
return new CheckerResult
{
Accepted = exitCode == 0,
ExitCode = exitCode,
Message = string.IsNullOrWhiteSpace(stderr) ? stdout : stderr,
Verdict = GetVerdictFromExitCode(exitCode)
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error running checker");
return new CheckerResult
{
Accepted = false,
ExitCode = -1,
Message = $"Checker error: {ex.Message}"
};
}
finally
{
// Cleanup temporary file
try
{
if (File.Exists(tempOutputPath))
{
File.Delete(tempOutputPath);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to delete temporary output file: {Path}", tempOutputPath);
}
}
}
private CheckerVerdict GetVerdictFromExitCode(int exitCode)
{
return exitCode switch
{
0 => CheckerVerdict.OK,
1 => CheckerVerdict.WrongAnswer,
2 => CheckerVerdict.PresentationError,
3 => CheckerVerdict.CheckerFailed,
7 => CheckerVerdict.PartialScore,
_ => CheckerVerdict.Unknown
};
}
}
/// <summary>
/// Result of custom checker execution
/// </summary>
public class CheckerResult
{
public bool Accepted { get; set; }
public int ExitCode { get; set; }
public string Message { get; set; } = string.Empty;
public CheckerVerdict Verdict { get; set; }
}
/// <summary>
/// Checker verdict codes (testlib standard)
/// </summary>
public enum CheckerVerdict
{
OK = 0, // Accepted
WrongAnswer = 1, // Wrong Answer
PresentationError = 2, // Presentation Error (format issue)
CheckerFailed = 3, // Internal checker error
PartialScore = 7, // Partial score (for IOI-style)
Unknown = -1 // Unknown exit code
}

View File

@@ -9,4 +9,14 @@ public interface IOutputCheckerService
/// <param name="expectedOutputPath">Path to expected output file</param>
/// <returns>True if outputs match</returns>
Task<bool> CheckOutputAsync(string actualOutput, string expectedOutputPath);
/// <summary>
/// Checks output using custom checker if available, falls back to standard checking
/// </summary>
/// <param name="actualOutput">Output from user's solution</param>
/// <param name="inputFilePath">Path to input file</param>
/// <param name="expectedOutputPath">Path to expected output file</param>
/// <param name="checkerPath">Path to custom checker executable (optional)</param>
/// <returns>True if output is accepted</returns>
Task<bool> CheckOutputWithCheckerAsync(string actualOutput, string inputFilePath, string expectedOutputPath, string? checkerPath);
}

View File

@@ -3,10 +3,12 @@ namespace LiquidCode.Tester.Worker.Services;
public class OutputCheckerService : IOutputCheckerService
{
private readonly ILogger<OutputCheckerService> _logger;
private readonly CheckerService _checkerService;
public OutputCheckerService(ILogger<OutputCheckerService> logger)
public OutputCheckerService(ILogger<OutputCheckerService> logger, CheckerService checkerService)
{
_logger = logger;
_checkerService = checkerService;
}
public async Task<bool> CheckOutputAsync(string actualOutput, string expectedOutputPath)
@@ -51,4 +53,35 @@ public class OutputCheckerService : IOutputCheckerService
return string.Join("\n", lines);
}
public async Task<bool> CheckOutputWithCheckerAsync(
string actualOutput,
string inputFilePath,
string expectedOutputPath,
string? checkerPath)
{
// If custom checker is available, use it
if (!string.IsNullOrEmpty(checkerPath) && File.Exists(checkerPath))
{
_logger.LogDebug("Using custom checker: {CheckerPath}", checkerPath);
var checkerResult = await _checkerService.CheckAsync(
checkerPath,
inputFilePath,
actualOutput,
expectedOutputPath);
if (!checkerResult.Accepted)
{
_logger.LogWarning("Custom checker verdict: {Verdict} - {Message}",
checkerResult.Verdict, checkerResult.Message);
}
return checkerResult.Accepted;
}
// Fall back to standard string comparison
_logger.LogDebug("No custom checker, using standard comparison");
return await CheckOutputAsync(actualOutput, expectedOutputPath);
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,141 @@
using System.Xml.Linq;
using LiquidCode.Tester.Common.Models;
namespace LiquidCode.Tester.Worker.Services;
/// <summary>
/// Parser for Polygon problem.xml format
/// </summary>
public class PolygonProblemXmlParser
{
private readonly ILogger<PolygonProblemXmlParser> _logger;
public PolygonProblemXmlParser(ILogger<PolygonProblemXmlParser> logger)
{
_logger = logger;
}
public PolygonProblemDescriptor? ParseProblemXml(string xmlPath)
{
try
{
var doc = XDocument.Load(xmlPath);
var problem = doc.Element("problem");
if (problem == null)
{
_logger.LogWarning("Invalid problem.xml: root 'problem' element not found");
return null;
}
var judging = problem.Element("judging");
if (judging == null)
{
_logger.LogWarning("No 'judging' section found in problem.xml");
return null;
}
var testset = judging.Element("testset");
if (testset == null)
{
_logger.LogWarning("No 'testset' section found in problem.xml");
return null;
}
var descriptor = new PolygonProblemDescriptor
{
ShortName = problem.Attribute("short-name")?.Value ?? "unknown",
Revision = int.TryParse(problem.Attribute("revision")?.Value, out var rev) ? rev : 0
};
// Parse time limit (in milliseconds)
var timeLimitText = testset.Element("time-limit")?.Value;
if (int.TryParse(timeLimitText, out var timeLimit))
{
descriptor.TimeLimitMs = timeLimit;
}
// Parse memory limit (in bytes)
var memoryLimitText = testset.Element("memory-limit")?.Value;
if (long.TryParse(memoryLimitText, out var memoryLimit))
{
descriptor.MemoryLimitMb = (int)(memoryLimit / (1024 * 1024)); // Convert bytes to MB
}
// Parse test count
var testCountText = testset.Element("test-count")?.Value;
if (int.TryParse(testCountText, out var testCount))
{
descriptor.TestCount = testCount;
}
// Parse path patterns
descriptor.InputPathPattern = testset.Element("input-path-pattern")?.Value ?? "tests/%02d";
descriptor.AnswerPathPattern = testset.Element("answer-path-pattern")?.Value ?? "tests/%02d.a";
// Parse solutions to find main solution
var assets = problem.Element("assets");
if (assets != null)
{
var solutions = assets.Element("solutions");
if (solutions != null)
{
// Try to find main solution first
var mainSolution = solutions.Elements("solution")
.FirstOrDefault(s => s.Attribute("tag")?.Value == "main");
// If no main solution, try to find any accepted solution
if (mainSolution == null)
{
mainSolution = solutions.Elements("solution")
.FirstOrDefault(s => s.Attribute("tag")?.Value == "accepted");
}
if (mainSolution != null)
{
var source = mainSolution.Element("source");
if (source != null)
{
descriptor.MainSolutionPath = source.Attribute("path")?.Value;
descriptor.MainSolutionType = source.Attribute("type")?.Value;
_logger.LogInformation("Found main solution: {Path} (type: {Type})",
descriptor.MainSolutionPath, descriptor.MainSolutionType);
}
}
else
{
_logger.LogWarning("No main or accepted solution found in problem.xml");
}
}
}
_logger.LogInformation(
"Parsed problem.xml: {ShortName} (rev {Revision}), {TestCount} tests, TL={TimeLimit}ms, ML={MemoryLimit}MB",
descriptor.ShortName, descriptor.Revision, descriptor.TestCount, descriptor.TimeLimitMs, descriptor.MemoryLimitMb);
return descriptor;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to parse problem.xml at {Path}", xmlPath);
return null;
}
}
}
/// <summary>
/// Descriptor parsed from problem.xml
/// </summary>
public class PolygonProblemDescriptor
{
public string ShortName { get; set; } = string.Empty;
public int Revision { get; set; }
public int TimeLimitMs { get; set; } = 2000;
public int MemoryLimitMb { get; set; } = 256;
public int TestCount { get; set; }
public string InputPathPattern { get; set; } = "tests/%02d";
public string AnswerPathPattern { get; set; } = "tests/%02d.a";
public string? MainSolutionPath { get; set; }
public string? MainSolutionType { get; set; }
}

View File

@@ -58,6 +58,16 @@ public class TestingService : ITestingService
_logger.LogInformation("Package parsed, found {TestCount} tests", package.TestCases.Count);
// Validate that package contains test cases
if (package.TestCases.Count == 0)
{
_logger.LogError("No test cases found in package for submit {SubmitId}", request.Id);
await SendStatusAsync(request, State.Done, ErrorCode.UnknownError,
"No test cases found in package", 0, 0);
CleanupWorkingDirectory(package.WorkingDirectory);
return;
}
// Send compiling status
await SendStatusAsync(request, State.Compiling, ErrorCode.None, "Compiling solution", 0, package.TestCases.Count);
@@ -125,8 +135,12 @@ public class TestingService : ITestingService
return;
}
// Check output
var outputCorrect = await _outputChecker.CheckOutputAsync(executionResult.Output, testCase.OutputFilePath);
// Check output (using custom checker if available)
var outputCorrect = await _outputChecker.CheckOutputWithCheckerAsync(
executionResult.Output,
testCase.InputFilePath,
testCase.OutputFilePath,
package.CheckerPath);
if (!outputCorrect)
{