Adds package caching to the testing service
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 1m31s
Build and Push Docker Images / build (src/LiquidCode.Tester.Worker/Dockerfile, git.nullptr.top/liquidcode/liquidcode-tester-worker-roman, worker) (push) Successful in 4m44s
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 1m31s
Build and Push Docker Images / build (src/LiquidCode.Tester.Worker/Dockerfile, git.nullptr.top/liquidcode/liquidcode-tester-worker-roman, worker) (push) Successful in 4m44s
Implements a package cache to avoid reparsing and extracting problem packages for subsequent submissions, improving performance and reducing resource consumption. Introduces an interface and a concurrent dictionary-based implementation for the cache. A processing lock is also implemented using a semaphore to avoid concurrent access to the same package.
This commit is contained in:
@@ -13,6 +13,7 @@ builder.Services.AddHttpClient();
|
|||||||
builder.Services.AddSingleton<PolygonProblemXmlParser>();
|
builder.Services.AddSingleton<PolygonProblemXmlParser>();
|
||||||
builder.Services.AddSingleton<AnswerGenerationService>();
|
builder.Services.AddSingleton<AnswerGenerationService>();
|
||||||
builder.Services.AddSingleton<CheckerService>();
|
builder.Services.AddSingleton<CheckerService>();
|
||||||
|
builder.Services.AddSingleton<IPackageCacheService, PackageCacheService>();
|
||||||
builder.Services.AddSingleton<IPackageParserService, PackageParserService>();
|
builder.Services.AddSingleton<IPackageParserService, PackageParserService>();
|
||||||
builder.Services.AddSingleton<IOutputCheckerService, OutputCheckerService>();
|
builder.Services.AddSingleton<IOutputCheckerService, OutputCheckerService>();
|
||||||
builder.Services.AddSingleton<ICallbackService, CallbackService>();
|
builder.Services.AddSingleton<ICallbackService, CallbackService>();
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using LiquidCode.Tester.Common.Models;
|
||||||
|
|
||||||
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
|
public interface IPackageCacheService
|
||||||
|
{
|
||||||
|
Task<CachedPackageResult> GetOrAddAsync(string cacheKey, Func<Task<ProblemPackage>> factory);
|
||||||
|
ValueTask<IAsyncDisposable> AcquireProcessingLockAsync(string cacheKey, CancellationToken cancellationToken = default);
|
||||||
|
void Invalidate(string cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record CachedPackageResult(ProblemPackage Package, bool IsFromCache, string CacheKey)
|
||||||
|
{
|
||||||
|
public static CachedPackageResult FromNonCached(ProblemPackage package)
|
||||||
|
=> new(package, false, string.Empty);
|
||||||
|
}
|
||||||
111
src/LiquidCode.Tester.Worker/Services/PackageCacheService.cs
Normal file
111
src/LiquidCode.Tester.Worker/Services/PackageCacheService.cs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using LiquidCode.Tester.Common.Models;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
|
public class PackageCacheService : IPackageCacheService
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<string, Lazy<Task<ProblemPackage>>> _cache = new(StringComparer.Ordinal);
|
||||||
|
private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new(StringComparer.Ordinal);
|
||||||
|
private readonly ILogger<PackageCacheService> _logger;
|
||||||
|
|
||||||
|
public PackageCacheService(ILogger<PackageCacheService>? logger = null)
|
||||||
|
{
|
||||||
|
_logger = logger ?? NullLogger<PackageCacheService>.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CachedPackageResult> GetOrAddAsync(string cacheKey, Func<Task<ProblemPackage>> factory)
|
||||||
|
{
|
||||||
|
if (factory == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(factory));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(cacheKey))
|
||||||
|
{
|
||||||
|
var package = await factory().ConfigureAwait(false);
|
||||||
|
return CachedPackageResult.FromNonCached(package);
|
||||||
|
}
|
||||||
|
|
||||||
|
var lazyFactory = new Lazy<Task<ProblemPackage>>(() => factory(), LazyThreadSafetyMode.ExecutionAndPublication);
|
||||||
|
var lazy = _cache.GetOrAdd(cacheKey, lazyFactory);
|
||||||
|
var fromCache = lazy != lazyFactory;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var package = await lazy.Value.ConfigureAwait(false);
|
||||||
|
return new CachedPackageResult(package, fromCache, cacheKey);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
if (!fromCache)
|
||||||
|
{
|
||||||
|
_cache.TryRemove(cacheKey, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<IAsyncDisposable> AcquireProcessingLockAsync(string cacheKey, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(cacheKey))
|
||||||
|
{
|
||||||
|
return NoopAsyncDisposable.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
var semaphore = _locks.GetOrAdd(cacheKey, _ => new SemaphoreSlim(1, 1));
|
||||||
|
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
return new SemaphoreReleaser(semaphore);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Invalidate(string cacheKey)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(cacheKey))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_cache.TryRemove(cacheKey, out _))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Invalidated package cache entry {CacheKey}", cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
_locks.TryRemove(cacheKey, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SemaphoreReleaser : IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly SemaphoreSlim _semaphore;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public SemaphoreReleaser(SemaphoreSlim semaphore)
|
||||||
|
{
|
||||||
|
_semaphore = semaphore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
_semaphore.Release();
|
||||||
|
_disposed = true;
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NoopAsyncDisposable : IAsyncDisposable
|
||||||
|
{
|
||||||
|
public static readonly NoopAsyncDisposable Instance = new();
|
||||||
|
private NoopAsyncDisposable() { }
|
||||||
|
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using LiquidCode.Tester.Common.Models;
|
using LiquidCode.Tester.Common.Models;
|
||||||
using LiquidCode.Tester.Worker.Controllers;
|
using LiquidCode.Tester.Worker.Controllers;
|
||||||
|
|
||||||
@@ -6,6 +8,7 @@ namespace LiquidCode.Tester.Worker.Services;
|
|||||||
public class TestingService : ITestingService
|
public class TestingService : ITestingService
|
||||||
{
|
{
|
||||||
private readonly IPackageParserService _packageParser;
|
private readonly IPackageParserService _packageParser;
|
||||||
|
private readonly IPackageCacheService _packageCache;
|
||||||
private readonly ICompilationServiceFactory _compilationServiceFactory;
|
private readonly ICompilationServiceFactory _compilationServiceFactory;
|
||||||
private readonly IExecutionServiceFactory _executionServiceFactory;
|
private readonly IExecutionServiceFactory _executionServiceFactory;
|
||||||
private readonly IOutputCheckerService _outputChecker;
|
private readonly IOutputCheckerService _outputChecker;
|
||||||
@@ -14,6 +17,7 @@ public class TestingService : ITestingService
|
|||||||
|
|
||||||
public TestingService(
|
public TestingService(
|
||||||
IPackageParserService packageParser,
|
IPackageParserService packageParser,
|
||||||
|
IPackageCacheService packageCache,
|
||||||
ICompilationServiceFactory compilationServiceFactory,
|
ICompilationServiceFactory compilationServiceFactory,
|
||||||
IExecutionServiceFactory executionServiceFactory,
|
IExecutionServiceFactory executionServiceFactory,
|
||||||
IOutputCheckerService outputChecker,
|
IOutputCheckerService outputChecker,
|
||||||
@@ -21,6 +25,7 @@ public class TestingService : ITestingService
|
|||||||
ILogger<TestingService> logger)
|
ILogger<TestingService> logger)
|
||||||
{
|
{
|
||||||
_packageParser = packageParser;
|
_packageParser = packageParser;
|
||||||
|
_packageCache = packageCache;
|
||||||
_compilationServiceFactory = compilationServiceFactory;
|
_compilationServiceFactory = compilationServiceFactory;
|
||||||
_executionServiceFactory = executionServiceFactory;
|
_executionServiceFactory = executionServiceFactory;
|
||||||
_outputChecker = outputChecker;
|
_outputChecker = outputChecker;
|
||||||
@@ -32,66 +37,62 @@ public class TestingService : ITestingService
|
|||||||
{
|
{
|
||||||
_logger.LogInformation("Starting to process submit {SubmitId}", request.Id);
|
_logger.LogInformation("Starting to process submit {SubmitId}", request.Id);
|
||||||
|
|
||||||
|
string cleanupDirectory = string.Empty;
|
||||||
|
bool shouldCleanup = false;
|
||||||
|
|
||||||
|
void CleanupPackageIfNeeded()
|
||||||
|
{
|
||||||
|
if (shouldCleanup && !string.IsNullOrEmpty(cleanupDirectory))
|
||||||
|
{
|
||||||
|
CleanupWorkingDirectory(cleanupDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Send initial status
|
|
||||||
await SendStatusAsync(request, State.Waiting, ErrorCode.None, "Submit received", 0, 0);
|
await SendStatusAsync(request, State.Waiting, ErrorCode.None, "Submit received", 0, 0);
|
||||||
|
|
||||||
// Parse package
|
var packageResult = await GetPackageAsync(request);
|
||||||
ProblemPackage package;
|
var package = packageResult.Package;
|
||||||
if (!string.IsNullOrEmpty(request.PackageFilePath))
|
cleanupDirectory = package.ExtractionRoot ?? package.WorkingDirectory;
|
||||||
{
|
shouldCleanup = string.IsNullOrEmpty(packageResult.CacheKey);
|
||||||
// Use saved file path (from background task)
|
|
||||||
await using var fileStream = File.OpenRead(request.PackageFilePath);
|
await using var packageProcessingLock = await _packageCache.AcquireProcessingLockAsync(packageResult.CacheKey);
|
||||||
package = await _packageParser.ParsePackageAsync(fileStream);
|
|
||||||
}
|
|
||||||
else if (request.Package != null)
|
|
||||||
{
|
|
||||||
// Use IFormFile directly (should not happen in background tasks)
|
|
||||||
using var packageStream = request.Package.OpenReadStream();
|
|
||||||
package = await _packageParser.ParsePackageAsync(packageStream);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("No package provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Package parsed, found {TestCount} tests", package.TestCases.Count);
|
_logger.LogInformation("Package parsed, found {TestCount} tests", package.TestCases.Count);
|
||||||
|
|
||||||
// Validate that package contains test cases
|
|
||||||
if (package.TestCases.Count == 0)
|
if (package.TestCases.Count == 0)
|
||||||
{
|
{
|
||||||
_logger.LogError("No test cases found in package for submit {SubmitId}", request.Id);
|
_logger.LogError("No test cases found in package for submit {SubmitId}", request.Id);
|
||||||
await SendStatusAsync(request, State.Done, ErrorCode.UnknownError,
|
await SendStatusAsync(request, State.Done, ErrorCode.UnknownError,
|
||||||
"No test cases found in package", 0, 0);
|
"No test cases found in package", 0, 0);
|
||||||
CleanupWorkingDirectory(package.ExtractionRoot ?? package.WorkingDirectory);
|
CleanupPackageIfNeeded();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send compiling status
|
|
||||||
await SendStatusAsync(request, State.Compiling, ErrorCode.None, "Compiling solution", 0, package.TestCases.Count);
|
await SendStatusAsync(request, State.Compiling, ErrorCode.None, "Compiling solution", 0, package.TestCases.Count);
|
||||||
|
|
||||||
// Get language-specific services
|
|
||||||
var compilationService = _compilationServiceFactory.GetCompilationService(request.Language);
|
var compilationService = _compilationServiceFactory.GetCompilationService(request.Language);
|
||||||
var executionService = _executionServiceFactory.GetExecutionService(request.Language);
|
var executionService = _executionServiceFactory.GetExecutionService(request.Language);
|
||||||
|
|
||||||
// Compile user solution
|
var compilationResult = await compilationService.CompileAsync(
|
||||||
var compilationResult = await compilationService.CompileAsync(request.SourceCode, package.WorkingDirectory, request.LanguageVersion);
|
request.SourceCode,
|
||||||
|
package.WorkingDirectory,
|
||||||
|
request.LanguageVersion);
|
||||||
|
|
||||||
if (!compilationResult.Success)
|
if (!compilationResult.Success)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Compilation failed for submit {SubmitId}", request.Id);
|
_logger.LogWarning("Compilation failed for submit {SubmitId}", request.Id);
|
||||||
await SendStatusAsync(request, State.Done, ErrorCode.CompileError,
|
await SendStatusAsync(request, State.Done, ErrorCode.CompileError,
|
||||||
$"Compilation failed: {compilationResult.CompilerOutput}", 0, package.TestCases.Count);
|
$"Compilation failed: {compilationResult.CompilerOutput}", 0, package.TestCases.Count);
|
||||||
|
CleanupPackageIfNeeded();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Compilation successful");
|
_logger.LogInformation("Compilation successful");
|
||||||
|
|
||||||
// Send testing status
|
|
||||||
await SendStatusAsync(request, State.Testing, ErrorCode.None, "Running tests", 0, package.TestCases.Count);
|
await SendStatusAsync(request, State.Testing, ErrorCode.None, "Running tests", 0, package.TestCases.Count);
|
||||||
|
|
||||||
// Run tests
|
|
||||||
for (int i = 0; i < package.TestCases.Count; i++)
|
for (int i = 0; i < package.TestCases.Count; i++)
|
||||||
{
|
{
|
||||||
var testCase = package.TestCases[i];
|
var testCase = package.TestCases[i];
|
||||||
@@ -100,20 +101,18 @@ public class TestingService : ITestingService
|
|||||||
await SendStatusAsync(request, State.Testing, ErrorCode.None,
|
await SendStatusAsync(request, State.Testing, ErrorCode.None,
|
||||||
$"Running test {testCase.Number}", testCase.Number, package.TestCases.Count);
|
$"Running test {testCase.Number}", testCase.Number, package.TestCases.Count);
|
||||||
|
|
||||||
// Execute solution
|
|
||||||
var executionResult = await executionService.ExecuteAsync(
|
var executionResult = await executionService.ExecuteAsync(
|
||||||
compilationResult.ExecutablePath!,
|
compilationResult.ExecutablePath!,
|
||||||
testCase.InputFilePath,
|
testCase.InputFilePath,
|
||||||
testCase.TimeLimit,
|
testCase.TimeLimit,
|
||||||
testCase.MemoryLimit);
|
testCase.MemoryLimit);
|
||||||
|
|
||||||
// Check for execution errors
|
|
||||||
if (executionResult.TimeLimitExceeded)
|
if (executionResult.TimeLimitExceeded)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Time limit exceeded on test {TestNumber}", testCase.Number);
|
_logger.LogWarning("Time limit exceeded on test {TestNumber}", testCase.Number);
|
||||||
await SendStatusAsync(request, State.Done, ErrorCode.TimeLimitError,
|
await SendStatusAsync(request, State.Done, ErrorCode.TimeLimitError,
|
||||||
$"Time limit exceeded on test {testCase.Number}", testCase.Number, package.TestCases.Count);
|
$"Time limit exceeded on test {testCase.Number}", testCase.Number, package.TestCases.Count);
|
||||||
CleanupWorkingDirectory(package.ExtractionRoot ?? package.WorkingDirectory);
|
CleanupPackageIfNeeded();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +121,7 @@ public class TestingService : ITestingService
|
|||||||
_logger.LogWarning("Memory limit exceeded on test {TestNumber}", testCase.Number);
|
_logger.LogWarning("Memory limit exceeded on test {TestNumber}", testCase.Number);
|
||||||
await SendStatusAsync(request, State.Done, ErrorCode.MemoryError,
|
await SendStatusAsync(request, State.Done, ErrorCode.MemoryError,
|
||||||
$"Memory limit exceeded on test {testCase.Number}", testCase.Number, package.TestCases.Count);
|
$"Memory limit exceeded on test {testCase.Number}", testCase.Number, package.TestCases.Count);
|
||||||
CleanupWorkingDirectory(package.ExtractionRoot ?? package.WorkingDirectory);
|
CleanupPackageIfNeeded();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,11 +130,10 @@ public class TestingService : ITestingService
|
|||||||
_logger.LogWarning("Runtime error on test {TestNumber}: {Error}", testCase.Number, executionResult.ErrorMessage);
|
_logger.LogWarning("Runtime error on test {TestNumber}: {Error}", testCase.Number, executionResult.ErrorMessage);
|
||||||
await SendStatusAsync(request, State.Done, ErrorCode.RuntimeError,
|
await SendStatusAsync(request, State.Done, ErrorCode.RuntimeError,
|
||||||
$"Runtime error on test {testCase.Number}: {executionResult.ErrorMessage}", testCase.Number, package.TestCases.Count);
|
$"Runtime error on test {testCase.Number}: {executionResult.ErrorMessage}", testCase.Number, package.TestCases.Count);
|
||||||
CleanupWorkingDirectory(package.ExtractionRoot ?? package.WorkingDirectory);
|
CleanupPackageIfNeeded();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check output (using custom checker if available)
|
|
||||||
var outputCorrect = await _outputChecker.CheckOutputWithCheckerAsync(
|
var outputCorrect = await _outputChecker.CheckOutputWithCheckerAsync(
|
||||||
executionResult.Output,
|
executionResult.Output,
|
||||||
testCase.InputFilePath,
|
testCase.InputFilePath,
|
||||||
@@ -147,29 +145,71 @@ public class TestingService : ITestingService
|
|||||||
_logger.LogWarning("Wrong answer on test {TestNumber}", testCase.Number);
|
_logger.LogWarning("Wrong answer on test {TestNumber}", testCase.Number);
|
||||||
await SendStatusAsync(request, State.Done, ErrorCode.IncorrectAnswer,
|
await SendStatusAsync(request, State.Done, ErrorCode.IncorrectAnswer,
|
||||||
$"Wrong answer on test {testCase.Number}", testCase.Number, package.TestCases.Count);
|
$"Wrong answer on test {testCase.Number}", testCase.Number, package.TestCases.Count);
|
||||||
CleanupWorkingDirectory(package.ExtractionRoot ?? package.WorkingDirectory);
|
CleanupPackageIfNeeded();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Test {TestNumber} passed", testCase.Number);
|
_logger.LogInformation("Test {TestNumber} passed", testCase.Number);
|
||||||
}
|
}
|
||||||
|
|
||||||
// All tests passed!
|
|
||||||
_logger.LogInformation("All tests passed for submit {SubmitId}", request.Id);
|
_logger.LogInformation("All tests passed for submit {SubmitId}", request.Id);
|
||||||
await SendStatusAsync(request, State.Done, ErrorCode.None,
|
await SendStatusAsync(request, State.Done, ErrorCode.None,
|
||||||
"All tests passed", package.TestCases.Count, package.TestCases.Count);
|
"All tests passed", package.TestCases.Count, package.TestCases.Count);
|
||||||
|
|
||||||
// Cleanup
|
CleanupPackageIfNeeded();
|
||||||
CleanupWorkingDirectory(package.ExtractionRoot ?? package.WorkingDirectory);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error processing submit {SubmitId}", request.Id);
|
_logger.LogError(ex, "Error processing submit {SubmitId}", request.Id);
|
||||||
await SendStatusAsync(request, State.Done, ErrorCode.UnknownError,
|
await SendStatusAsync(request, State.Done, ErrorCode.UnknownError,
|
||||||
$"Internal error: {ex.Message}", 0, 0);
|
$"Internal error: {ex.Message}", 0, 0);
|
||||||
|
CleanupPackageIfNeeded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<CachedPackageResult> GetPackageAsync(TestRequest request)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(request.PackageFilePath))
|
||||||
|
{
|
||||||
|
if (!File.Exists(request.PackageFilePath))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException($"Package file not found at {request.PackageFilePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheKey = await ComputePackageCacheKeyAsync(request.PackageFilePath, request.MissionId);
|
||||||
|
return await _packageCache.GetOrAddAsync(cacheKey, () => ParsePackageFromFileAsync(request.PackageFilePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.Package != null)
|
||||||
|
{
|
||||||
|
await using var packageStream = request.Package.OpenReadStream();
|
||||||
|
var package = await _packageParser.ParsePackageAsync(packageStream);
|
||||||
|
return CachedPackageResult.FromNonCached(package);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("No package provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ProblemPackage> ParsePackageFromFileAsync(string packageFilePath)
|
||||||
|
{
|
||||||
|
await using var fileStream = File.OpenRead(packageFilePath);
|
||||||
|
return await _packageParser.ParsePackageAsync(fileStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> ComputePackageCacheKeyAsync(string packageFilePath, long missionId)
|
||||||
|
{
|
||||||
|
await using var stream = File.OpenRead(packageFilePath);
|
||||||
|
var hash = await ComputeHashAsync(stream);
|
||||||
|
return $"{missionId}:{hash}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string> ComputeHashAsync(Stream stream)
|
||||||
|
{
|
||||||
|
using var sha256 = SHA256.Create();
|
||||||
|
var hash = await sha256.ComputeHashAsync(stream);
|
||||||
|
return Convert.ToHexString(hash);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task SendStatusAsync(TestRequest request, State state, ErrorCode errorCode, string message, int currentTest, int totalTests)
|
private async Task SendStatusAsync(TestRequest request, State state, ErrorCode errorCode, string message, int currentTest, int totalTests)
|
||||||
{
|
{
|
||||||
var response = new TesterResponseModel(
|
var response = new TesterResponseModel(
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public class TestingServiceTests : IDisposable
|
|||||||
private readonly Mock<IOutputCheckerService> _outputCheckerMock;
|
private readonly Mock<IOutputCheckerService> _outputCheckerMock;
|
||||||
private readonly Mock<ICallbackService> _callbackServiceMock;
|
private readonly Mock<ICallbackService> _callbackServiceMock;
|
||||||
private readonly Mock<ILogger<TestingService>> _loggerMock;
|
private readonly Mock<ILogger<TestingService>> _loggerMock;
|
||||||
|
private readonly IPackageCacheService _packageCache;
|
||||||
private readonly TestingService _service;
|
private readonly TestingService _service;
|
||||||
private readonly string _testDirectory;
|
private readonly string _testDirectory;
|
||||||
|
|
||||||
@@ -26,9 +27,11 @@ public class TestingServiceTests : IDisposable
|
|||||||
_outputCheckerMock = new Mock<IOutputCheckerService>();
|
_outputCheckerMock = new Mock<IOutputCheckerService>();
|
||||||
_callbackServiceMock = new Mock<ICallbackService>();
|
_callbackServiceMock = new Mock<ICallbackService>();
|
||||||
_loggerMock = new Mock<ILogger<TestingService>>();
|
_loggerMock = new Mock<ILogger<TestingService>>();
|
||||||
|
_packageCache = new PackageCacheService();
|
||||||
|
|
||||||
_service = new TestingService(
|
_service = new TestingService(
|
||||||
_packageParserMock.Object,
|
_packageParserMock.Object,
|
||||||
|
_packageCache,
|
||||||
_compilationFactoryMock.Object,
|
_compilationFactoryMock.Object,
|
||||||
_executionFactoryMock.Object,
|
_executionFactoryMock.Object,
|
||||||
_outputCheckerMock.Object,
|
_outputCheckerMock.Object,
|
||||||
@@ -104,7 +107,7 @@ public class TestingServiceTests : IDisposable
|
|||||||
await File.WriteAllTextAsync(inputFile, "test input");
|
await File.WriteAllTextAsync(inputFile, "test input");
|
||||||
await File.WriteAllTextAsync(outputFile, "expected output");
|
await File.WriteAllTextAsync(outputFile, "expected output");
|
||||||
await File.WriteAllTextAsync(executablePath, "dummy");
|
await File.WriteAllTextAsync(executablePath, "dummy");
|
||||||
await CreateEmptyPackage(packageFilePath);
|
await CreateEmptyPackage(packageFilePath);
|
||||||
|
|
||||||
var request = new TestRequest
|
var request = new TestRequest
|
||||||
{
|
{
|
||||||
@@ -194,11 +197,108 @@ public class TestingServiceTests : IDisposable
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CreateEmptyPackage(string filePath)
|
[Fact]
|
||||||
|
public async Task ProcessSubmitAsync_ReusesCachedPackage()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var packageFilePath = Path.Combine(_testDirectory, "cached_package.zip");
|
||||||
|
var inputFile = Path.Combine(_testDirectory, "1.in");
|
||||||
|
var outputFile = Path.Combine(_testDirectory, "1.out");
|
||||||
|
var executablePath = Path.Combine(_testDirectory, "solution.exe");
|
||||||
|
|
||||||
|
await File.WriteAllTextAsync(inputFile, "test input");
|
||||||
|
await File.WriteAllTextAsync(outputFile, "expected output");
|
||||||
|
await File.WriteAllTextAsync(executablePath, "dummy");
|
||||||
|
await CreateEmptyPackage(packageFilePath);
|
||||||
|
|
||||||
|
var request = new TestRequest
|
||||||
|
{
|
||||||
|
Id = 123,
|
||||||
|
MissionId = 456,
|
||||||
|
Language = "cpp",
|
||||||
|
LanguageVersion = "17",
|
||||||
|
SourceCode = "int main() { return 0; }",
|
||||||
|
PackageFilePath = packageFilePath,
|
||||||
|
CallbackUrl = "http://localhost/callback"
|
||||||
|
};
|
||||||
|
|
||||||
|
var package = new ProblemPackage
|
||||||
|
{
|
||||||
|
WorkingDirectory = _testDirectory,
|
||||||
|
ExtractionRoot = _testDirectory,
|
||||||
|
TestCases = new List<TestCase>
|
||||||
|
{
|
||||||
|
new TestCase
|
||||||
|
{
|
||||||
|
Number = 1,
|
||||||
|
InputFilePath = inputFile,
|
||||||
|
OutputFilePath = outputFile,
|
||||||
|
TimeLimit = 2000,
|
||||||
|
MemoryLimit = 256
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var compilationService = new Mock<ICompilationService>();
|
||||||
|
var executionService = new Mock<IExecutionService>();
|
||||||
|
|
||||||
|
var parseCalls = 0;
|
||||||
|
|
||||||
|
_packageParserMock
|
||||||
|
.Setup(x => x.ParsePackageAsync(It.IsAny<Stream>()))
|
||||||
|
.Callback(() => parseCalls++)
|
||||||
|
.ReturnsAsync(package);
|
||||||
|
|
||||||
|
_compilationFactoryMock
|
||||||
|
.Setup(x => x.GetCompilationService("cpp"))
|
||||||
|
.Returns(compilationService.Object);
|
||||||
|
|
||||||
|
_executionFactoryMock
|
||||||
|
.Setup(x => x.GetExecutionService("cpp"))
|
||||||
|
.Returns(executionService.Object);
|
||||||
|
|
||||||
|
compilationService
|
||||||
|
.Setup(x => x.CompileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.ReturnsAsync(new CompilationResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
ExecutablePath = executablePath
|
||||||
|
});
|
||||||
|
|
||||||
|
executionService
|
||||||
|
.Setup(x => x.ExecuteAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
|
||||||
|
.ReturnsAsync(new ExecutionResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Output = "expected output",
|
||||||
|
ExitCode = 0,
|
||||||
|
RuntimeError = false,
|
||||||
|
TimeLimitExceeded = false,
|
||||||
|
MemoryLimitExceeded = false
|
||||||
|
});
|
||||||
|
|
||||||
|
_outputCheckerMock
|
||||||
|
.Setup(x => x.CheckOutputWithCheckerAsync(
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<string?>()))
|
||||||
|
.ReturnsAsync(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _service.ProcessSubmitAsync(request);
|
||||||
|
await _service.ProcessSubmitAsync(request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(1, parseCalls);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task CreateEmptyPackage(string filePath)
|
||||||
{
|
{
|
||||||
using var fileStream = File.Create(filePath);
|
using var fileStream = File.Create(filePath);
|
||||||
using var archive = new ZipArchive(fileStream, ZipArchiveMode.Create);
|
using var archive = new ZipArchive(fileStream, ZipArchiveMode.Create);
|
||||||
// Create empty archive
|
// Create empty archive
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
Reference in New Issue
Block a user