Штуки
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
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
This commit is contained in:
@@ -136,23 +136,37 @@ public class AnswerGenerationService
|
||||
return (null, "");
|
||||
}
|
||||
|
||||
if (solutionType.StartsWith("python."))
|
||||
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)
|
||||
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."))
|
||||
{
|
||||
// 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
|
||||
var standard = ExtractCppStandard(solutionType);
|
||||
return ("cpp", standard);
|
||||
}
|
||||
|
||||
if (solutionType.StartsWith("java"))
|
||||
@@ -178,4 +192,22 @@ public class AnswerGenerationService
|
||||
_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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using LiquidCode.Tester.Worker.Models;
|
||||
|
||||
namespace LiquidCode.Tester.Worker.Services;
|
||||
@@ -23,57 +25,8 @@ public class CppCompilationService : ICompilationService
|
||||
|
||||
try
|
||||
{
|
||||
// Write source code to file
|
||||
await File.WriteAllTextAsync(sourceFilePath, sourceCode);
|
||||
|
||||
// Resolve version-specific configuration
|
||||
var (compiler, compilerFlags) = ResolveVersion(version);
|
||||
|
||||
_logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, compilerFlags);
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = compiler,
|
||||
Arguments = $"{compilerFlags} {sourceFilePath} -o {executablePath}",
|
||||
WorkingDirectory = workingDirectory,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
var output = await process.StandardOutput.ReadToEndAsync();
|
||||
var error = await process.StandardError.ReadToEndAsync();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
var compilerOutput = $"{output}\n{error}".Trim();
|
||||
|
||||
if (process.ExitCode == 0 && File.Exists(executablePath))
|
||||
{
|
||||
_logger.LogInformation("Compilation successful");
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = true,
|
||||
ExecutablePath = executablePath,
|
||||
CompilerOutput = compilerOutput
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Compilation failed with exit code {ExitCode}", process.ExitCode);
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Compilation failed",
|
||||
CompilerOutput = compilerOutput
|
||||
};
|
||||
}
|
||||
return await CompileFileAsync(sourceFilePath, executablePath, version);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -86,31 +39,168 @@ public class CppCompilationService : ICompilationService
|
||||
}
|
||||
}
|
||||
|
||||
private (string compiler, string compilerFlags) ResolveVersion(string? version)
|
||||
public async Task<CompilationResult> CompileFileAsync(
|
||||
string sourceFilePath,
|
||||
string outputExecutablePath,
|
||||
string? version = null,
|
||||
IEnumerable<string>? includeDirectories = null,
|
||||
IEnumerable<string>? additionalFlags = null)
|
||||
{
|
||||
// If version is null or "latest", use default configuration
|
||||
_logger.LogInformation("Compiling C++ source {Source} -> {Output} with version {Version}", sourceFilePath, outputExecutablePath, version ?? "latest");
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputExecutablePath)!);
|
||||
|
||||
var (compiler, compilerFlags) = ResolveVersion(version);
|
||||
|
||||
_logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, string.Join(' ', compilerFlags));
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = compiler,
|
||||
WorkingDirectory = Path.GetDirectoryName(sourceFilePath) ?? Directory.GetCurrentDirectory(),
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var flag in compilerFlags)
|
||||
{
|
||||
process.StartInfo.ArgumentList.Add(flag);
|
||||
}
|
||||
|
||||
if (includeDirectories != null)
|
||||
{
|
||||
foreach (var includeDir in includeDirectories.Where(d => !string.IsNullOrWhiteSpace(d)))
|
||||
{
|
||||
process.StartInfo.ArgumentList.Add($"-I{includeDir}");
|
||||
}
|
||||
}
|
||||
|
||||
if (additionalFlags != null)
|
||||
{
|
||||
foreach (var flag in additionalFlags.Where(f => !string.IsNullOrWhiteSpace(f)))
|
||||
{
|
||||
process.StartInfo.ArgumentList.Add(flag);
|
||||
}
|
||||
}
|
||||
|
||||
process.StartInfo.ArgumentList.Add(sourceFilePath);
|
||||
process.StartInfo.ArgumentList.Add("-o");
|
||||
process.StartInfo.ArgumentList.Add(outputExecutablePath);
|
||||
|
||||
process.Start();
|
||||
|
||||
var stdOutTask = process.StandardOutput.ReadToEndAsync();
|
||||
var stdErrTask = process.StandardError.ReadToEndAsync();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
var compilerOutput = $"{await stdOutTask}\n{await stdErrTask}".Trim();
|
||||
|
||||
if (process.ExitCode == 0 && File.Exists(outputExecutablePath))
|
||||
{
|
||||
_logger.LogInformation("Compilation successful");
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = true,
|
||||
ExecutablePath = outputExecutablePath,
|
||||
CompilerOutput = compilerOutput
|
||||
};
|
||||
}
|
||||
|
||||
_logger.LogWarning("Compilation failed with exit code {ExitCode}", process.ExitCode);
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Compilation failed",
|
||||
CompilerOutput = compilerOutput
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during compilation");
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = $"Compilation error: {ex.Message}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private (string compiler, List<string> compilerFlags) ResolveVersion(string? version)
|
||||
{
|
||||
var defaultCompiler = _configuration["Cpp:Compiler"] ?? "g++";
|
||||
var defaultFlags = SplitFlags(_configuration["Cpp:CompilerFlags"] ?? "-O2 -std=c++17 -Wall");
|
||||
|
||||
if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var compiler = _configuration["Cpp:Compiler"] ?? "g++";
|
||||
var compilerFlags = _configuration["Cpp:CompilerFlags"] ?? "-O2 -std=c++17 -Wall";
|
||||
return (compiler, compilerFlags);
|
||||
return (defaultCompiler, defaultFlags);
|
||||
}
|
||||
|
||||
// Try to find version-specific configuration
|
||||
var versionKey = $"Cpp:Versions:{version}";
|
||||
var versionCompiler = _configuration[$"{versionKey}:Compiler"];
|
||||
var versionFlags = _configuration[$"{versionKey}:CompilerFlags"];
|
||||
var versionFlagsValue = _configuration[$"{versionKey}:CompilerFlags"];
|
||||
|
||||
if (!string.IsNullOrEmpty(versionCompiler))
|
||||
if (!string.IsNullOrEmpty(versionCompiler) || !string.IsNullOrEmpty(versionFlagsValue))
|
||||
{
|
||||
var resolvedFlags = !string.IsNullOrEmpty(versionFlagsValue)
|
||||
? SplitFlags(versionFlagsValue)
|
||||
: defaultFlags;
|
||||
|
||||
_logger.LogInformation("Using C++ version {Version} configuration", version);
|
||||
return (versionCompiler, versionFlags ?? "-O2 -Wall");
|
||||
return (versionCompiler ?? defaultCompiler, resolvedFlags);
|
||||
}
|
||||
|
||||
var normalized = NormalizeCppVersion(version);
|
||||
if (normalized != null)
|
||||
{
|
||||
var flagsWithoutStd = defaultFlags
|
||||
.Where(flag => !flag.StartsWith("-std=", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
flagsWithoutStd.Add($"-std=c++{normalized}");
|
||||
_logger.LogInformation("Using inferred C++ standard c++{Standard}", normalized);
|
||||
return (defaultCompiler, flagsWithoutStd);
|
||||
}
|
||||
|
||||
// Version not found, use default and log warning
|
||||
_logger.LogWarning("C++ version {Version} not found in configuration, using default", version);
|
||||
var defaultCompiler = _configuration["Cpp:Compiler"] ?? "g++";
|
||||
var defaultFlags = _configuration["Cpp:CompilerFlags"] ?? "-O2 -std=c++17 -Wall";
|
||||
return (defaultCompiler, defaultFlags);
|
||||
}
|
||||
|
||||
private static List<string> SplitFlags(string flags) =>
|
||||
flags.Split(' ', StringSplitOptions.RemoveEmptyEntries)
|
||||
.ToList();
|
||||
|
||||
private static string? NormalizeCppVersion(string version)
|
||||
{
|
||||
var cleaned = version.Trim().ToLowerInvariant();
|
||||
cleaned = cleaned.Replace("c++", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("gnu++", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Trim('+', ' ');
|
||||
|
||||
cleaned = cleaned switch
|
||||
{
|
||||
"2b" => "23",
|
||||
"2a" => "20",
|
||||
"1z" => "17",
|
||||
"0x" => "11",
|
||||
_ => cleaned
|
||||
};
|
||||
|
||||
return cleaned switch
|
||||
{
|
||||
"26" => "26",
|
||||
"23" => "23",
|
||||
"20" => "20",
|
||||
"17" => "17",
|
||||
"14" => "14",
|
||||
"11" => "11",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,12 @@ public class OutputCheckerService : IOutputCheckerService
|
||||
lines.RemoveAt(lines.Count - 1);
|
||||
}
|
||||
|
||||
// Remove leading empty lines
|
||||
while (lines.Count > 0 && string.IsNullOrWhiteSpace(lines[0]))
|
||||
{
|
||||
lines.RemoveAt(0);
|
||||
}
|
||||
|
||||
return string.Join("\n", lines);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using LiquidCode.Tester.Common.Models;
|
||||
|
||||
namespace LiquidCode.Tester.Worker.Services;
|
||||
@@ -35,12 +41,15 @@ public class PackageParserService : IPackageParserService
|
||||
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))
|
||||
// Check if this is a Polygon package (search for problem.xml)
|
||||
var problemXmlPath = Directory.EnumerateFiles(workingDirectory, "problem.xml", SearchOption.AllDirectories)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (!string.IsNullOrEmpty(problemXmlPath))
|
||||
{
|
||||
_logger.LogInformation("Detected Polygon package format (problem.xml found)");
|
||||
return await ParsePolygonPackageAsync(workingDirectory, problemXmlPath);
|
||||
var packageRoot = Path.GetDirectoryName(problemXmlPath)!;
|
||||
_logger.LogInformation("Detected Polygon package format (problem.xml found at {ProblemXml})", problemXmlPath);
|
||||
return await ParsePolygonPackageAsync(packageRoot, problemXmlPath, workingDirectory);
|
||||
}
|
||||
|
||||
// Fall back to legacy format (.in/.out files)
|
||||
@@ -54,89 +63,120 @@ public class PackageParserService : IPackageParserService
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ProblemPackage> ParsePolygonPackageAsync(string workingDirectory, string problemXmlPath)
|
||||
private async Task<ProblemPackage> ParsePolygonPackageAsync(string packageRoot, string problemXmlPath, string extractionRoot)
|
||||
{
|
||||
var descriptor = _polygonParser.ParseProblemXml(problemXmlPath);
|
||||
|
||||
if (descriptor == null)
|
||||
{
|
||||
_logger.LogWarning("Failed to parse problem.xml, falling back to legacy format");
|
||||
return await ParseLegacyPackage(workingDirectory);
|
||||
return await ParseLegacyPackage(packageRoot, extractionRoot);
|
||||
}
|
||||
|
||||
var package = new ProblemPackage
|
||||
{
|
||||
WorkingDirectory = workingDirectory,
|
||||
WorkingDirectory = packageRoot,
|
||||
ExtractionRoot = extractionRoot,
|
||||
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>();
|
||||
var buildDirectory = Path.Combine(packageRoot, ".lc-build");
|
||||
Directory.CreateDirectory(buildDirectory);
|
||||
|
||||
for (int i = 1; i <= descriptor.TestCount; i++)
|
||||
var compiledExecutables = await CompilePolygonExecutablesAsync(descriptor, packageRoot, buildDirectory);
|
||||
|
||||
package.CheckerPath = await CompileCheckerForPackageAsync(descriptor, packageRoot, buildDirectory)
|
||||
?? await FindAndCompileCheckerAsync(packageRoot);
|
||||
|
||||
var validatorPath = await CompileValidatorAsync(descriptor, packageRoot, buildDirectory, compiledExecutables);
|
||||
|
||||
await GenerateAndValidateTestsAsync(descriptor, packageRoot, compiledExecutables, validatorPath);
|
||||
|
||||
var testIndices = descriptor.Tests.Count > 0
|
||||
? descriptor.Tests.Select(t => t.Index).Distinct().OrderBy(i => i).ToList()
|
||||
: Enumerable.Range(1, descriptor.TestCount > 0 ? descriptor.TestCount : 0).ToList();
|
||||
|
||||
if (testIndices.Count == 0)
|
||||
{
|
||||
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));
|
||||
_logger.LogWarning("No test definitions discovered in problem.xml; falling back to filesystem scan");
|
||||
return await ParseLegacyPackage(packageRoot, extractionRoot);
|
||||
}
|
||||
|
||||
if (!File.Exists(inputPath))
|
||||
var inputs = new List<(int index, string inputPath)>();
|
||||
var answers = new Dictionary<int, string>();
|
||||
|
||||
foreach (var testIndex in testIndices)
|
||||
{
|
||||
var inputRelative = FormatPolygonPattern(descriptor.InputPathPattern, testIndex);
|
||||
var inputFullPath = Path.Combine(packageRoot, NormalizeRelativePath(inputRelative));
|
||||
|
||||
if (!File.Exists(inputFullPath))
|
||||
{
|
||||
_logger.LogWarning("Input file not found: {InputPath}", inputPath);
|
||||
_logger.LogWarning("Input file not found for test {Index}: {RelativePath}", testIndex, inputRelative);
|
||||
continue;
|
||||
}
|
||||
|
||||
inputPaths.Add(inputPath);
|
||||
answerPaths.Add(answerPath);
|
||||
var answerRelative = FormatPolygonPattern(descriptor.AnswerPathPattern, testIndex);
|
||||
var answerFullPath = Path.Combine(packageRoot, NormalizeRelativePath(answerRelative));
|
||||
|
||||
inputs.Add((testIndex, inputFullPath));
|
||||
answers[testIndex] = answerFullPath;
|
||||
}
|
||||
|
||||
var missingAnswerInputs = new List<string>();
|
||||
var missingAnswerPaths = new List<string>();
|
||||
|
||||
foreach (var (index, inputPath) in inputs)
|
||||
{
|
||||
if (!answers.TryGetValue(index, out var answerPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!File.Exists(answerPath))
|
||||
{
|
||||
missingAnswerPaths.Add(answerPath);
|
||||
missingAnswerInputs.Add(inputPath);
|
||||
missingAnswerPaths.Add(answerPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
_logger.LogInformation("Found {Count} tests without answers, attempting to generate them", missingAnswerPaths.Count);
|
||||
|
||||
var generated = await _answerGenerator.GenerateAnswersAsync(
|
||||
descriptor,
|
||||
workingDirectory,
|
||||
packageRoot,
|
||||
missingAnswerInputs,
|
||||
missingAnswerPaths);
|
||||
|
||||
if (generated)
|
||||
if (!generated)
|
||||
{
|
||||
_logger.LogInformation("Successfully generated answer files");
|
||||
_logger.LogWarning("Failed to generate answer files, affected tests will be skipped");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Failed to generate answer files, tests without answers will be skipped");
|
||||
_logger.LogInformation("Answer files generated successfully");
|
||||
}
|
||||
}
|
||||
|
||||
// Now create test cases for all tests that have answer files
|
||||
for (int i = 0; i < inputPaths.Count; i++)
|
||||
foreach (var (index, inputPath) in inputs.OrderBy(item => item.index))
|
||||
{
|
||||
var inputPath = inputPaths[i];
|
||||
var answerPath = answerPaths[i];
|
||||
if (!answers.TryGetValue(index, out var answerPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!File.Exists(answerPath))
|
||||
{
|
||||
_logger.LogWarning("Answer file not found: {AnswerPath} (skipping test)", answerPath);
|
||||
_logger.LogWarning("Answer file not found for test {Index}: {AnswerPath}", index, answerPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
package.TestCases.Add(new TestCase
|
||||
{
|
||||
Number = i + 1,
|
||||
Number = index,
|
||||
InputFilePath = inputPath,
|
||||
OutputFilePath = answerPath,
|
||||
TimeLimit = descriptor.TimeLimitMs,
|
||||
@@ -144,26 +184,23 @@ public class PackageParserService : IPackageParserService
|
||||
});
|
||||
}
|
||||
|
||||
// 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.LogWarning("No test cases with answer files found for Polygon package");
|
||||
}
|
||||
|
||||
_logger.LogInformation("Parsed Polygon package with {TestCount} tests (out of {TotalTests} in problem.xml)",
|
||||
_logger.LogInformation("Parsed Polygon package with {TestCount} tests (declared {TotalTests})",
|
||||
package.TestCases.Count, descriptor.TestCount);
|
||||
|
||||
return package;
|
||||
}
|
||||
|
||||
private async Task<ProblemPackage> ParseLegacyPackage(string workingDirectory)
|
||||
private async Task<ProblemPackage> ParseLegacyPackage(string workingDirectory, string? extractionRoot = null)
|
||||
{
|
||||
var package = new ProblemPackage
|
||||
{
|
||||
WorkingDirectory = workingDirectory
|
||||
WorkingDirectory = workingDirectory,
|
||||
ExtractionRoot = extractionRoot ?? workingDirectory
|
||||
};
|
||||
|
||||
// Find tests directory
|
||||
@@ -193,7 +230,7 @@ public class PackageParserService : IPackageParserService
|
||||
|
||||
package.TestCases.Add(new TestCase
|
||||
{
|
||||
Number = i + 1,
|
||||
Number = package.TestCases.Count + 1,
|
||||
InputFilePath = inputFile,
|
||||
OutputFilePath = outputFile,
|
||||
TimeLimit = package.DefaultTimeLimit,
|
||||
@@ -213,6 +250,447 @@ public class PackageParserService : IPackageParserService
|
||||
return package;
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, string>> CompilePolygonExecutablesAsync(
|
||||
PolygonProblemDescriptor descriptor,
|
||||
string packageRoot,
|
||||
string buildDirectory)
|
||||
{
|
||||
var compiled = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var executable in descriptor.Executables)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(executable.SourcePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var alias = !string.IsNullOrWhiteSpace(executable.Name)
|
||||
? executable.Name
|
||||
: Path.GetFileNameWithoutExtension(executable.SourcePath);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(alias))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var sourceFullPath = Path.Combine(packageRoot, NormalizeRelativePath(executable.SourcePath));
|
||||
|
||||
if (!File.Exists(sourceFullPath))
|
||||
{
|
||||
_logger.LogWarning("Executable source not found: {Path}", executable.SourcePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsCppAsset(executable.Type, sourceFullPath))
|
||||
{
|
||||
var outputPath = Path.Combine(buildDirectory, alias);
|
||||
var includeDirs = GetIncludeDirectories(packageRoot, sourceFullPath);
|
||||
var stdVersion = ExtractCppStandard(executable.Type);
|
||||
|
||||
var compilationResult = await _cppCompilation.CompileFileAsync(
|
||||
sourceFullPath,
|
||||
outputPath,
|
||||
stdVersion,
|
||||
includeDirs);
|
||||
|
||||
if (compilationResult.Success && !string.IsNullOrEmpty(compilationResult.ExecutablePath))
|
||||
{
|
||||
_logger.LogInformation("Compiled executable {Alias} -> {Path}", alias, compilationResult.ExecutablePath);
|
||||
compiled[alias] = compilationResult.ExecutablePath;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Failed to compile executable {Alias}: {Error}", alias, compilationResult.CompilerOutput);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var binaryPath = !string.IsNullOrWhiteSpace(executable.BinaryPath)
|
||||
? Path.Combine(packageRoot, NormalizeRelativePath(executable.BinaryPath))
|
||||
: null;
|
||||
|
||||
if (!string.IsNullOrEmpty(binaryPath) && File.Exists(binaryPath))
|
||||
{
|
||||
_logger.LogInformation("Using prebuilt executable {Alias} at {Path}", alias, binaryPath);
|
||||
compiled[alias] = binaryPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Unsupported executable type {Type} for {Alias}", executable.Type, alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return compiled;
|
||||
}
|
||||
|
||||
private async Task<string?> CompileCheckerForPackageAsync(
|
||||
PolygonProblemDescriptor descriptor,
|
||||
string packageRoot,
|
||||
string buildDirectory)
|
||||
{
|
||||
if (descriptor.Checker == null || string.IsNullOrWhiteSpace(descriptor.Checker.SourcePath))
|
||||
{
|
||||
_logger.LogInformation("No checker declared in problem.xml");
|
||||
return null;
|
||||
}
|
||||
|
||||
var sourcePath = Path.Combine(packageRoot, NormalizeRelativePath(descriptor.Checker.SourcePath));
|
||||
|
||||
if (!File.Exists(sourcePath))
|
||||
{
|
||||
_logger.LogWarning("Checker source not found: {SourcePath}", descriptor.Checker.SourcePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
var alias = !string.IsNullOrWhiteSpace(descriptor.Checker.CopyPath)
|
||||
? Path.GetFileNameWithoutExtension(descriptor.Checker.CopyPath)
|
||||
: Path.GetFileNameWithoutExtension(descriptor.Checker.BinaryPath ?? descriptor.Checker.SourcePath);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(alias))
|
||||
{
|
||||
alias = "checker";
|
||||
}
|
||||
|
||||
var outputPath = Path.Combine(buildDirectory, alias);
|
||||
var includeDirs = GetIncludeDirectories(packageRoot, sourcePath);
|
||||
var stdVersion = ExtractCppStandard(descriptor.Checker.Type);
|
||||
|
||||
var result = await _cppCompilation.CompileFileAsync(
|
||||
sourcePath,
|
||||
outputPath,
|
||||
stdVersion,
|
||||
includeDirs);
|
||||
|
||||
if (result.Success && !string.IsNullOrEmpty(result.ExecutablePath))
|
||||
{
|
||||
_logger.LogInformation("Checker compiled to {Path}", result.ExecutablePath);
|
||||
return result.ExecutablePath;
|
||||
}
|
||||
|
||||
_logger.LogWarning("Failed to compile checker: {Error}", result.CompilerOutput);
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<string?> CompileValidatorAsync(
|
||||
PolygonProblemDescriptor descriptor,
|
||||
string packageRoot,
|
||||
string buildDirectory,
|
||||
IDictionary<string, string> compiledExecutables)
|
||||
{
|
||||
var validator = descriptor.Validators.FirstOrDefault();
|
||||
if (validator == null || string.IsNullOrWhiteSpace(validator.SourcePath))
|
||||
{
|
||||
_logger.LogInformation("No validator declared in problem.xml");
|
||||
return null;
|
||||
}
|
||||
|
||||
var sourcePath = Path.Combine(packageRoot, NormalizeRelativePath(validator.SourcePath));
|
||||
if (!File.Exists(sourcePath))
|
||||
{
|
||||
_logger.LogWarning("Validator source not found: {SourcePath}", validator.SourcePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
var alias = Path.GetFileNameWithoutExtension(validator.BinaryPath ?? validator.SourcePath);
|
||||
if (string.IsNullOrWhiteSpace(alias))
|
||||
{
|
||||
alias = "validator";
|
||||
}
|
||||
|
||||
if (compiledExecutables.TryGetValue(alias, out var compiledPath) && File.Exists(compiledPath))
|
||||
{
|
||||
_logger.LogInformation("Reusing precompiled validator executable at {Path}", compiledPath);
|
||||
return compiledPath;
|
||||
}
|
||||
|
||||
var outputPath = Path.Combine(buildDirectory, alias);
|
||||
var includeDirs = GetIncludeDirectories(packageRoot, sourcePath);
|
||||
var stdVersion = ExtractCppStandard(validator.Type);
|
||||
|
||||
var result = await _cppCompilation.CompileFileAsync(
|
||||
sourcePath,
|
||||
outputPath,
|
||||
stdVersion,
|
||||
includeDirs);
|
||||
|
||||
if (result.Success && !string.IsNullOrEmpty(result.ExecutablePath))
|
||||
{
|
||||
_logger.LogInformation("Validator compiled to {Path}", result.ExecutablePath);
|
||||
compiledExecutables[alias] = result.ExecutablePath;
|
||||
return result.ExecutablePath;
|
||||
}
|
||||
|
||||
_logger.LogWarning("Failed to compile validator: {Error}", result.CompilerOutput);
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task GenerateAndValidateTestsAsync(
|
||||
PolygonProblemDescriptor descriptor,
|
||||
string packageRoot,
|
||||
IDictionary<string, string> compiledExecutables,
|
||||
string? validatorPath)
|
||||
{
|
||||
if (descriptor.Tests.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("problem.xml does not enumerate tests; skipping generation step");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var test in descriptor.Tests)
|
||||
{
|
||||
var inputRelative = FormatPolygonPattern(descriptor.InputPathPattern, test.Index);
|
||||
var inputFullPath = Path.Combine(packageRoot, NormalizeRelativePath(inputRelative));
|
||||
|
||||
if (test.Method is PolygonTestMethod.Generated or PolygonTestMethod.Script)
|
||||
{
|
||||
await GenerateTestInputAsync(test, packageRoot, inputFullPath, compiledExecutables);
|
||||
}
|
||||
else if (!File.Exists(inputFullPath))
|
||||
{
|
||||
_logger.LogWarning("Manual test {Index} expected at {Path} but not found", test.Index, inputRelative);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!File.Exists(inputFullPath))
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to produce input file for test {test.Index} ({inputRelative})");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(validatorPath))
|
||||
{
|
||||
await ValidateInputAsync(validatorPath, inputFullPath, test.Index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task GenerateTestInputAsync(
|
||||
PolygonTestDefinition test,
|
||||
string packageRoot,
|
||||
string inputFullPath,
|
||||
IDictionary<string, string> compiledExecutables)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(test.Command))
|
||||
{
|
||||
_logger.LogWarning("Test {Index} is marked as generated but has no command", test.Index);
|
||||
return;
|
||||
}
|
||||
|
||||
var args = SplitCommandLine(test.Command);
|
||||
if (args.Count == 0)
|
||||
{
|
||||
_logger.LogWarning("Failed to parse generator command for test {Index}", test.Index);
|
||||
return;
|
||||
}
|
||||
|
||||
var generatorAlias = args[0];
|
||||
|
||||
if (!compiledExecutables.TryGetValue(generatorAlias, out var generatorPath))
|
||||
{
|
||||
_logger.LogWarning("Generator {Generator} not found for test {Index}", generatorAlias, test.Index);
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(inputFullPath)!);
|
||||
|
||||
_logger.LogInformation("Generating test {Index} using {Generator} {Arguments}",
|
||||
test.Index,
|
||||
generatorAlias,
|
||||
string.Join(' ', args.Skip(1)));
|
||||
|
||||
using var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = generatorPath,
|
||||
WorkingDirectory = packageRoot,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var argument in args.Skip(1))
|
||||
{
|
||||
process.StartInfo.ArgumentList.Add(argument);
|
||||
}
|
||||
|
||||
process.Start();
|
||||
|
||||
var stderrTask = process.StandardError.ReadToEndAsync();
|
||||
|
||||
await using (var outputStream = File.Create(inputFullPath))
|
||||
{
|
||||
await process.StandardOutput.BaseStream.CopyToAsync(outputStream);
|
||||
}
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
var stderr = await stderrTask;
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Generator '{generatorAlias}' failed for test {test.Index}: {stderr}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(stderr))
|
||||
{
|
||||
_logger.LogDebug("Generator '{Generator}' stderr for test {Index}: {Message}", generatorAlias, test.Index, stderr.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidateInputAsync(string validatorPath, string inputFilePath, int testIndex)
|
||||
{
|
||||
using var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = validatorPath,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
var inputContent = await File.ReadAllTextAsync(inputFilePath);
|
||||
var normalizedInput = inputContent.Replace("\r\n", "\n").Replace("\r", "\n");
|
||||
await process.StandardInput.WriteAsync(normalizedInput);
|
||||
process.StandardInput.Close();
|
||||
|
||||
var stdoutTask = process.StandardOutput.ReadToEndAsync();
|
||||
var stderrTask = process.StandardError.ReadToEndAsync();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
var stdout = await stdoutTask;
|
||||
var stderr = await stderrTask;
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
var message = string.IsNullOrWhiteSpace(stderr) ? stdout : stderr;
|
||||
throw new InvalidOperationException($"Validator rejected test {testIndex}: {message?.Trim()}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(stdout))
|
||||
{
|
||||
_logger.LogDebug("Validator output for test {Index}: {Message}", testIndex, stdout.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetIncludeDirectories(string packageRoot, string sourceFullPath)
|
||||
{
|
||||
var sourceDirectory = Path.GetDirectoryName(sourceFullPath);
|
||||
var filesDirectory = Path.Combine(packageRoot, "files");
|
||||
|
||||
return new[] { sourceDirectory, filesDirectory, packageRoot }
|
||||
.Where(dir => !string.IsNullOrWhiteSpace(dir))
|
||||
.Select(dir => dir!)
|
||||
.Where(Directory.Exists)
|
||||
.Distinct();
|
||||
}
|
||||
|
||||
private static string NormalizeRelativePath(string relativePath)
|
||||
{
|
||||
return relativePath
|
||||
.Replace("\\", Path.DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture))
|
||||
.Replace("/", Path.DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
private static string FormatPolygonPattern(string pattern, int index)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pattern))
|
||||
{
|
||||
return index.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return Regex.Replace(pattern, "%0?(\\d*)d", match =>
|
||||
{
|
||||
if (int.TryParse(match.Groups[1].Value, out var width) && width > 0)
|
||||
{
|
||||
return index.ToString($"D{width}", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return index.ToString(CultureInfo.InvariantCulture);
|
||||
});
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> SplitCommandLine(string command)
|
||||
{
|
||||
var result = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(command))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var current = new StringBuilder();
|
||||
var inQuotes = false;
|
||||
|
||||
foreach (var ch in command)
|
||||
{
|
||||
if (ch == '"')
|
||||
{
|
||||
inQuotes = !inQuotes;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char.IsWhiteSpace(ch) && !inQuotes)
|
||||
{
|
||||
if (current.Length > 0)
|
||||
{
|
||||
result.Add(current.ToString());
|
||||
current.Clear();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
current.Append(ch);
|
||||
}
|
||||
}
|
||||
|
||||
if (current.Length > 0)
|
||||
{
|
||||
result.Add(current.ToString());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool IsCppAsset(string? type, string sourceFullPath)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(type) && type.StartsWith("cpp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(sourceFullPath);
|
||||
return extension.Equals(".cpp", StringComparison.OrdinalIgnoreCase) ||
|
||||
extension.Equals(".cc", StringComparison.OrdinalIgnoreCase) ||
|
||||
extension.Equals(".cxx", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string? ExtractCppStandard(string? type)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(type))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalized = type.ToLowerInvariant();
|
||||
|
||||
if (normalized.Contains("++26")) return "26";
|
||||
if (normalized.Contains("++23")) return "23";
|
||||
if (normalized.Contains("++20")) return "20";
|
||||
if (normalized.Contains("++17")) return "17";
|
||||
if (normalized.Contains("++14")) return "14";
|
||||
if (normalized.Contains("++11")) return "11";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string? FindCorrespondingOutputFile(string inputFile)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(inputFile)!;
|
||||
@@ -278,14 +756,16 @@ public class PackageParserService : IPackageParserService
|
||||
|
||||
try
|
||||
{
|
||||
var checkerSource = await File.ReadAllTextAsync(candidate);
|
||||
var checkerDir = Path.GetDirectoryName(candidate)!;
|
||||
var outputPath = Path.Combine(checkerDir, Path.GetFileNameWithoutExtension(candidate));
|
||||
var includeDirs = GetIncludeDirectories(workingDirectory, candidate);
|
||||
|
||||
// Compile checker with C++17 (testlib.h compatible)
|
||||
var compilationResult = await _cppCompilation.CompileAsync(
|
||||
checkerSource,
|
||||
checkerDir,
|
||||
"17");
|
||||
// Compile checker with inferred standard (default C++17)
|
||||
var compilationResult = await _cppCompilation.CompileFileAsync(
|
||||
candidate,
|
||||
outputPath,
|
||||
"17",
|
||||
includeDirs);
|
||||
|
||||
if (!compilationResult.Success)
|
||||
{
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using LiquidCode.Tester.Common.Models;
|
||||
|
||||
@@ -73,7 +76,89 @@ public class PolygonProblemXmlParser
|
||||
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
|
||||
// Parse detailed test definitions (if present)
|
||||
var testsElement = testset.Element("tests");
|
||||
if (testsElement != null)
|
||||
{
|
||||
var index = 1;
|
||||
foreach (var testElement in testsElement.Elements("test"))
|
||||
{
|
||||
var methodValue = testElement.Attribute("method")?.Value ?? "manual";
|
||||
var method = methodValue.ToLowerInvariant() switch
|
||||
{
|
||||
"generated" => PolygonTestMethod.Generated,
|
||||
"manual" => PolygonTestMethod.Manual,
|
||||
"script" => PolygonTestMethod.Script,
|
||||
_ => PolygonTestMethod.Unknown
|
||||
};
|
||||
|
||||
double? points = null;
|
||||
var pointsAttr = testElement.Attribute("points")?.Value;
|
||||
if (!string.IsNullOrWhiteSpace(pointsAttr) &&
|
||||
double.TryParse(pointsAttr, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedPoints))
|
||||
{
|
||||
points = parsedPoints;
|
||||
}
|
||||
|
||||
var definition = new PolygonTestDefinition
|
||||
{
|
||||
Index = index,
|
||||
Method = method,
|
||||
Command = testElement.Attribute("cmd")?.Value,
|
||||
Group = testElement.Attribute("group")?.Value,
|
||||
IsSample = bool.TryParse(testElement.Attribute("sample")?.Value, out var sample) && sample,
|
||||
Points = points,
|
||||
Comment = testElement.Attribute("comment")?.Value
|
||||
};
|
||||
|
||||
descriptor.Tests.Add(definition);
|
||||
index++;
|
||||
}
|
||||
|
||||
if (descriptor.TestCount == 0)
|
||||
{
|
||||
descriptor.TestCount = descriptor.Tests.Count;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse auxiliary executables defined in <files><executables>
|
||||
var filesSection = problem.Element("files");
|
||||
if (filesSection != null)
|
||||
{
|
||||
var executablesSection = filesSection.Element("executables");
|
||||
if (executablesSection != null)
|
||||
{
|
||||
foreach (var executableElement in executablesSection.Elements("executable"))
|
||||
{
|
||||
var sourceElement = executableElement.Element("source");
|
||||
if (sourceElement == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var sourcePath = sourceElement.Attribute("path")?.Value;
|
||||
if (string.IsNullOrWhiteSpace(sourcePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var binaryPath = executableElement.Element("binary")?.Attribute("path")?.Value;
|
||||
var name = !string.IsNullOrWhiteSpace(binaryPath)
|
||||
? Path.GetFileNameWithoutExtension(binaryPath)
|
||||
: Path.GetFileNameWithoutExtension(sourcePath);
|
||||
|
||||
descriptor.Executables.Add(new PolygonExecutableDescriptor
|
||||
{
|
||||
Name = name,
|
||||
SourcePath = sourcePath,
|
||||
BinaryPath = binaryPath,
|
||||
Type = sourceElement.Attribute("type")?.Value
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse assets: solutions, checker, validators
|
||||
var assets = problem.Element("assets");
|
||||
if (assets != null)
|
||||
{
|
||||
@@ -108,6 +193,44 @@ public class PolygonProblemXmlParser
|
||||
_logger.LogWarning("No main or accepted solution found in problem.xml");
|
||||
}
|
||||
}
|
||||
|
||||
var checkerElement = assets.Element("checker");
|
||||
if (checkerElement != null)
|
||||
{
|
||||
var checkerSource = checkerElement.Element("source");
|
||||
if (checkerSource != null)
|
||||
{
|
||||
descriptor.Checker = new PolygonCheckerDescriptor
|
||||
{
|
||||
SourcePath = checkerSource.Attribute("path")?.Value ?? string.Empty,
|
||||
Type = checkerSource.Attribute("type")?.Value,
|
||||
BinaryPath = checkerElement.Element("binary")?.Attribute("path")?.Value,
|
||||
CopyPath = checkerElement.Element("copy")?.Attribute("path")?.Value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var validatorsSection = assets.Element("validators");
|
||||
if (validatorsSection != null)
|
||||
{
|
||||
foreach (var validatorElement in validatorsSection.Elements("validator"))
|
||||
{
|
||||
var validatorSource = validatorElement.Element("source");
|
||||
if (validatorSource == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var validator = new PolygonValidatorDescriptor
|
||||
{
|
||||
SourcePath = validatorSource.Attribute("path")?.Value ?? string.Empty,
|
||||
Type = validatorSource.Attribute("type")?.Value,
|
||||
BinaryPath = validatorElement.Element("binary")?.Attribute("path")?.Value
|
||||
};
|
||||
|
||||
descriptor.Validators.Add(validator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
@@ -138,4 +261,62 @@ public class PolygonProblemDescriptor
|
||||
public string AnswerPathPattern { get; set; } = "tests/%02d.a";
|
||||
public string? MainSolutionPath { get; set; }
|
||||
public string? MainSolutionType { get; set; }
|
||||
public List<PolygonTestDefinition> Tests { get; } = new();
|
||||
public List<PolygonExecutableDescriptor> Executables { get; } = new();
|
||||
public PolygonCheckerDescriptor? Checker { get; set; }
|
||||
public List<PolygonValidatorDescriptor> Validators { get; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single test entry defined in problem.xml
|
||||
/// </summary>
|
||||
public class PolygonTestDefinition
|
||||
{
|
||||
public int Index { get; set; }
|
||||
public PolygonTestMethod Method { get; set; } = PolygonTestMethod.Manual;
|
||||
public string? Command { get; set; }
|
||||
public string? Group { get; set; }
|
||||
public bool IsSample { get; set; }
|
||||
public double? Points { get; set; }
|
||||
public string? Comment { get; set; }
|
||||
}
|
||||
|
||||
public enum PolygonTestMethod
|
||||
{
|
||||
Manual,
|
||||
Generated,
|
||||
Script,
|
||||
Unknown
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents additional executables (generators, validators, printers) declared in problem.xml
|
||||
/// </summary>
|
||||
public class PolygonExecutableDescriptor
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string SourcePath { get; set; } = string.Empty;
|
||||
public string? BinaryPath { get; set; }
|
||||
public string? Type { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents checker information declared in problem.xml
|
||||
/// </summary>
|
||||
public class PolygonCheckerDescriptor
|
||||
{
|
||||
public string SourcePath { get; set; } = string.Empty;
|
||||
public string? BinaryPath { get; set; }
|
||||
public string? CopyPath { get; set; }
|
||||
public string? Type { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents validator information declared in problem.xml
|
||||
/// </summary>
|
||||
public class PolygonValidatorDescriptor
|
||||
{
|
||||
public string SourcePath { get; set; } = string.Empty;
|
||||
public string? BinaryPath { get; set; }
|
||||
public string? Type { get; set; }
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ public class TestingService : ITestingService
|
||||
_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);
|
||||
CleanupWorkingDirectory(package.ExtractionRoot ?? package.WorkingDirectory);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ public class TestingService : ITestingService
|
||||
_logger.LogWarning("Time limit exceeded on test {TestNumber}", testCase.Number);
|
||||
await SendStatusAsync(request, State.Done, ErrorCode.TimeLimitError,
|
||||
$"Time limit exceeded on test {testCase.Number}", testCase.Number, package.TestCases.Count);
|
||||
CleanupWorkingDirectory(package.WorkingDirectory);
|
||||
CleanupWorkingDirectory(package.ExtractionRoot ?? package.WorkingDirectory);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ public class TestingService : ITestingService
|
||||
_logger.LogWarning("Memory limit exceeded on test {TestNumber}", testCase.Number);
|
||||
await SendStatusAsync(request, State.Done, ErrorCode.MemoryError,
|
||||
$"Memory limit exceeded on test {testCase.Number}", testCase.Number, package.TestCases.Count);
|
||||
CleanupWorkingDirectory(package.WorkingDirectory);
|
||||
CleanupWorkingDirectory(package.ExtractionRoot ?? package.WorkingDirectory);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ public class TestingService : ITestingService
|
||||
_logger.LogWarning("Runtime error on test {TestNumber}: {Error}", testCase.Number, executionResult.ErrorMessage);
|
||||
await SendStatusAsync(request, State.Done, ErrorCode.RuntimeError,
|
||||
$"Runtime error on test {testCase.Number}: {executionResult.ErrorMessage}", testCase.Number, package.TestCases.Count);
|
||||
CleanupWorkingDirectory(package.WorkingDirectory);
|
||||
CleanupWorkingDirectory(package.ExtractionRoot ?? package.WorkingDirectory);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ public class TestingService : ITestingService
|
||||
_logger.LogWarning("Wrong answer on test {TestNumber}", testCase.Number);
|
||||
await SendStatusAsync(request, State.Done, ErrorCode.IncorrectAnswer,
|
||||
$"Wrong answer on test {testCase.Number}", testCase.Number, package.TestCases.Count);
|
||||
CleanupWorkingDirectory(package.WorkingDirectory);
|
||||
CleanupWorkingDirectory(package.ExtractionRoot ?? package.WorkingDirectory);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ public class TestingService : ITestingService
|
||||
"All tests passed", package.TestCases.Count, package.TestCases.Count);
|
||||
|
||||
// Cleanup
|
||||
CleanupWorkingDirectory(package.WorkingDirectory);
|
||||
CleanupWorkingDirectory(package.ExtractionRoot ?? package.WorkingDirectory);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user