Files
LiquidCode.Tester/tests/LiquidCode.Tester.Worker.Tests/PolygonPackageIntegrationTests.cs
Roman Pytkov 147c95a48a
All checks were successful
Build and Push Docker Images / build (src/LiquidCode.Tester.Gateway/Dockerfile, git.nullptr.top/liquidcode/liquidcode-tester-gateway-roman, gateway) (push) Successful in 53s
Build and Push Docker Images / build (src/LiquidCode.Tester.Worker/Dockerfile, git.nullptr.top/liquidcode/liquidcode-tester-worker-roman, worker) (push) Successful in 3m51s
Enables code execution within a Docker sandbox
Adds a Docker-based execution sandbox to enhance the security and resource management of code execution.

This change introduces:
- New execution services for C#, C++, Java, Kotlin, and Python that utilize the sandbox.
- Configuration options for enabling/disabling the sandbox and specifying Docker images for different languages.
- Batch execution support for C++ to improve the efficiency of generating answer files.
- Docker CLI installation in the worker's Dockerfile.

The sandbox provides improved isolation and resource control during code execution, preventing potential security vulnerabilities and resource exhaustion.
2025-11-05 01:18:12 +03:00

209 lines
7.3 KiB
C#

using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using LiquidCode.Tester.Worker.Services;
using LiquidCode.Tester.Worker.Services.Sandbox;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Xunit;
namespace LiquidCode.Tester.Worker.Tests;
public class PolygonPackageIntegrationTests
{
private readonly PackageParserService _parserService;
private readonly SandboxOptions _sandboxOptions;
public PolygonPackageIntegrationTests()
{
var configuration = BuildConfiguration();
var loggerFactory = LoggerFactory.Create(builder =>
{
builder.SetMinimumLevel(LogLevel.Debug);
builder.AddConsole();
});
_sandboxOptions = new SandboxOptions();
configuration.GetSection("Sandbox").Bind(_sandboxOptions);
if (!_sandboxOptions.Enabled)
{
_sandboxOptions.Enabled = true;
}
var cppCompilation = new CppCompilationService(loggerFactory.CreateLogger<CppCompilationService>(), configuration);
var sandbox = new ExecutionSandbox(Options.Create(_sandboxOptions), loggerFactory.CreateLogger<ExecutionSandbox>());
var cppExecution = new CppExecutionService(sandbox, loggerFactory.CreateLogger<CppExecutionService>());
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(configuration);
services.AddSingleton(cppCompilation);
services.AddSingleton<IExecutionSandbox>(sandbox);
services.AddSingleton(cppExecution);
var serviceProvider = services.BuildServiceProvider();
var compilationFactory = new CompilationServiceFactory(serviceProvider, loggerFactory.CreateLogger<CompilationServiceFactory>());
var executionFactory = new ExecutionServiceFactory(serviceProvider, loggerFactory.CreateLogger<ExecutionServiceFactory>());
var answerGenerator = new AnswerGenerationService(compilationFactory, executionFactory, loggerFactory.CreateLogger<AnswerGenerationService>());
var polygonParser = new PolygonProblemXmlParser(loggerFactory.CreateLogger<PolygonProblemXmlParser>());
_parserService = new PackageParserService(
loggerFactory.CreateLogger<PackageParserService>(),
polygonParser,
answerGenerator,
cppCompilation);
}
[Fact]
public async Task ParsePolygonPackageAsync_GeneratesTestsCheckerAndAnswers()
{
if (!IsExecutableAvailable("g++"))
{
return;
}
var dockerExecutable = string.IsNullOrWhiteSpace(_sandboxOptions.DockerExecutable)
? "docker"
: _sandboxOptions.DockerExecutable;
if (!IsExecutableAvailable(dockerExecutable))
{
return;
}
Assert.True(
_sandboxOptions.Languages.TryGetValue("cpp", out var cppLanguage) &&
!string.IsNullOrWhiteSpace(cppLanguage.ExecutionImage),
"Sandbox configuration must specify an execution image for C++.");
var packageDirectory = ResolvePackageDirectory("exam-queue-17");
Assert.True(Directory.Exists(packageDirectory));
await using var packageStream = CreateZipFromDirectory(packageDirectory);
var package = await _parserService.ParsePackageAsync(packageStream);
try
{
Assert.Equal(51, package.TestCases.Count);
Assert.NotNull(package.CheckerPath);
Assert.True(File.Exists(package.CheckerPath), $"Checker not present at {package.CheckerPath}");
foreach (var testCase in package.TestCases)
{
Assert.True(File.Exists(testCase.InputFilePath), $"Missing input {testCase.InputFilePath}");
Assert.True(File.Exists(testCase.OutputFilePath), $"Missing output {testCase.OutputFilePath}");
Assert.True(new FileInfo(testCase.InputFilePath).Length > 0);
Assert.True(new FileInfo(testCase.OutputFilePath).Length > 0);
}
}
finally
{
if (!KeepTemporaryExtraction())
{
if (!string.IsNullOrEmpty(package.ExtractionRoot) && Directory.Exists(package.ExtractionRoot))
{
Directory.Delete(package.ExtractionRoot, recursive: true);
}
else if (Directory.Exists(package.WorkingDirectory))
{
Directory.Delete(package.WorkingDirectory, recursive: true);
}
}
}
}
private static bool IsExecutableAvailable(string executable)
{
try
{
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = executable,
Arguments = "--version",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
if (!process.WaitForExit(5000))
{
try
{
process.Kill(entireProcessTree: true);
}
catch
{
// ignored
}
return false;
}
return process.ExitCode == 0;
}
catch
{
return false;
}
}
private static MemoryStream CreateZipFromDirectory(string directoryPath)
{
var memoryStream = new MemoryStream();
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, leaveOpen: true))
{
foreach (var file in Directory.GetFiles(directoryPath, "*", SearchOption.AllDirectories))
{
var entryName = Path.GetRelativePath(directoryPath, file).Replace("\\", "/");
var entry = archive.CreateEntry(entryName, CompressionLevel.Fastest);
using (var entryStream = entry.Open())
using (var fileStream = File.OpenRead(file))
{
fileStream.CopyTo(entryStream);
}
}
}
memoryStream.Position = 0;
return memoryStream;
}
private static IConfiguration BuildConfiguration()
{
var repositoryRoot = ResolveRepositoryRoot();
return new ConfigurationBuilder()
.SetBasePath(repositoryRoot)
.AddJsonFile("src/LiquidCode.Tester.Worker/appsettings.json", optional: false)
.AddJsonFile("src/LiquidCode.Tester.Worker/appsettings.Development.json", optional: true)
.AddEnvironmentVariables()
.Build();
}
private static string ResolveRepositoryRoot()
{
var baseDirectory = AppContext.BaseDirectory;
return Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "..", ".."));
}
private static string ResolvePackageDirectory(string folderName)
{
return Path.Combine(ResolveRepositoryRoot(), folderName);
}
private static bool KeepTemporaryExtraction() =>
string.Equals(Environment.GetEnvironmentVariable("LC_KEEP_POLYGON_TEMP"), "1", StringComparison.Ordinal);
}