diff --git a/.idea/.idea.LiquidCode.Tester/.idea/workspace.xml b/.idea/.idea.LiquidCode.Tester/.idea/workspace.xml index 6662305..7406874 100644 --- a/.idea/.idea.LiquidCode.Tester/.idea/workspace.xml +++ b/.idea/.idea.LiquidCode.Tester/.idea/workspace.xml @@ -12,12 +12,14 @@ - - - - - - + + + + + + + + @@ -272,8 +283,21 @@ + + + diff --git a/global.json b/global.json index 93681ff..33d0916 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "9.0.0", + "version": "10.0.100-rc.2.25502.107", "rollForward": "latestMinor", - "allowPrerelease": false + "allowPrerelease": true } } \ No newline at end of file diff --git a/src/LiquidCode.Tester.Common/LiquidCode.Tester.Common.csproj b/src/LiquidCode.Tester.Common/LiquidCode.Tester.Common.csproj index e8c41ec..a5dfc1f 100644 --- a/src/LiquidCode.Tester.Common/LiquidCode.Tester.Common.csproj +++ b/src/LiquidCode.Tester.Common/LiquidCode.Tester.Common.csproj @@ -1,11 +1,9 @@  - Exe net9.0 enable enable - Linux diff --git a/src/LiquidCode.Tester.Common/Models/ErrorCode.cs b/src/LiquidCode.Tester.Common/Models/ErrorCode.cs new file mode 100644 index 0000000..5c0aa54 --- /dev/null +++ b/src/LiquidCode.Tester.Common/Models/ErrorCode.cs @@ -0,0 +1,12 @@ +namespace LiquidCode.Tester.Common.Models; + +public enum ErrorCode +{ + None, + CompileError, + RuntimeError, + MemoryError, + TimeLimitError, + IncorrectAnswer, + UnknownError +} diff --git a/src/LiquidCode.Tester.Common/Models/State.cs b/src/LiquidCode.Tester.Common/Models/State.cs new file mode 100644 index 0000000..e98cc00 --- /dev/null +++ b/src/LiquidCode.Tester.Common/Models/State.cs @@ -0,0 +1,9 @@ +namespace LiquidCode.Tester.Common.Models; + +public enum State +{ + Waiting, + Compiling, + Testing, + Done +} diff --git a/src/LiquidCode.Tester.Common/Models/SubmitForTesterModel.cs b/src/LiquidCode.Tester.Common/Models/SubmitForTesterModel.cs new file mode 100644 index 0000000..9434997 --- /dev/null +++ b/src/LiquidCode.Tester.Common/Models/SubmitForTesterModel.cs @@ -0,0 +1,11 @@ +namespace LiquidCode.Tester.Common.Models; + +public record SubmitForTesterModel( + long Id, + long MissionId, + string Language, + string LanguageVersion, + string SourceCode, + string PackageUrl, + string CallbackUrl +); diff --git a/src/LiquidCode.Tester.Common/Models/TesterResponseModel.cs b/src/LiquidCode.Tester.Common/Models/TesterResponseModel.cs new file mode 100644 index 0000000..52aa778 --- /dev/null +++ b/src/LiquidCode.Tester.Common/Models/TesterResponseModel.cs @@ -0,0 +1,10 @@ +namespace LiquidCode.Tester.Common.Models; + +public record TesterResponseModel( + long SubmitId, + State State, + ErrorCode ErrorCode, + string Message, + int CurrentTest, + int AmountOfTests +); diff --git a/src/LiquidCode.Tester.Common/Program.cs b/src/LiquidCode.Tester.Common/Program.cs deleted file mode 100644 index e5dff12..0000000 --- a/src/LiquidCode.Tester.Common/Program.cs +++ /dev/null @@ -1,3 +0,0 @@ -// See https://aka.ms/new-console-template for more information - -Console.WriteLine("Hello, World!"); \ No newline at end of file diff --git a/src/LiquidCode.Tester.Gateway/Controllers/TesterController.cs b/src/LiquidCode.Tester.Gateway/Controllers/TesterController.cs new file mode 100644 index 0000000..b94527f --- /dev/null +++ b/src/LiquidCode.Tester.Gateway/Controllers/TesterController.cs @@ -0,0 +1,52 @@ +using LiquidCode.Tester.Common.Models; +using LiquidCode.Tester.Gateway.Services; +using Microsoft.AspNetCore.Mvc; + +namespace LiquidCode.Tester.Gateway.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class TesterController : ControllerBase +{ + private readonly IPackageDownloadService _packageDownloadService; + private readonly IWorkerClientService _workerClientService; + private readonly ILogger _logger; + + public TesterController( + IPackageDownloadService packageDownloadService, + IWorkerClientService workerClientService, + ILogger logger) + { + _packageDownloadService = packageDownloadService; + _workerClientService = workerClientService; + _logger = logger; + } + + [HttpPost("submit")] + public async Task Submit([FromBody] SubmitForTesterModel request) + { + _logger.LogInformation("Received submit request for ID {SubmitId}", request.Id); + + try + { + // Download the package + var packagePath = await _packageDownloadService.DownloadPackageAsync(request.PackageUrl); + + // Send to appropriate worker based on language + await _workerClientService.SendToWorkerAsync(request, packagePath); + + return Accepted(new { message = "Submit accepted for testing", submitId = request.Id }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to process submit {SubmitId}", request.Id); + return StatusCode(500, new { error = "Failed to process submit", details = ex.Message }); + } + } + + [HttpGet("health")] + public IActionResult Health() + { + return Ok(new { status = "healthy", timestamp = DateTime.UtcNow }); + } +} diff --git a/src/LiquidCode.Tester.Gateway/LiquidCode.Tester.Gateway.csproj b/src/LiquidCode.Tester.Gateway/LiquidCode.Tester.Gateway.csproj index bb9f7eb..c2a6c33 100644 --- a/src/LiquidCode.Tester.Gateway/LiquidCode.Tester.Gateway.csproj +++ b/src/LiquidCode.Tester.Gateway/LiquidCode.Tester.Gateway.csproj @@ -11,6 +11,10 @@ + + + + .dockerignore diff --git a/src/LiquidCode.Tester.Gateway/Program.cs b/src/LiquidCode.Tester.Gateway/Program.cs index d5e0ef3..5bb4655 100644 --- a/src/LiquidCode.Tester.Gateway/Program.cs +++ b/src/LiquidCode.Tester.Gateway/Program.cs @@ -1,41 +1,26 @@ +using LiquidCode.Tester.Gateway.Services; + var builder = WebApplication.CreateBuilder(args); -// Add services to the container. -// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +// Add services to the container +builder.Services.AddControllers(); builder.Services.AddOpenApi(); +// Add HttpClient +builder.Services.AddHttpClient(); + +// Register application services +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + var app = builder.Build(); -// Configure the HTTP request pipeline. +// Configure the HTTP request pipeline if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } -app.UseHttpsRedirection(); - -var summaries = new[] -{ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" -}; - -app.MapGet("/weatherforecast", () => - { - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] - )) - .ToArray(); - return forecast; - }) - .WithName("GetWeatherForecast"); +app.MapControllers(); app.Run(); - -record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) -{ - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); -} \ No newline at end of file diff --git a/src/LiquidCode.Tester.Gateway/Services/IPackageDownloadService.cs b/src/LiquidCode.Tester.Gateway/Services/IPackageDownloadService.cs new file mode 100644 index 0000000..1c23152 --- /dev/null +++ b/src/LiquidCode.Tester.Gateway/Services/IPackageDownloadService.cs @@ -0,0 +1,11 @@ +namespace LiquidCode.Tester.Gateway.Services; + +public interface IPackageDownloadService +{ + /// + /// Downloads a package from the specified URL + /// + /// URL to download the package from + /// Path to the downloaded package file + Task DownloadPackageAsync(string packageUrl); +} diff --git a/src/LiquidCode.Tester.Gateway/Services/IWorkerClientService.cs b/src/LiquidCode.Tester.Gateway/Services/IWorkerClientService.cs new file mode 100644 index 0000000..2d49696 --- /dev/null +++ b/src/LiquidCode.Tester.Gateway/Services/IWorkerClientService.cs @@ -0,0 +1,13 @@ +using LiquidCode.Tester.Common.Models; + +namespace LiquidCode.Tester.Gateway.Services; + +public interface IWorkerClientService +{ + /// + /// Sends a submit to the appropriate worker based on the language + /// + /// Submit data + /// Local path to the downloaded package + Task SendToWorkerAsync(SubmitForTesterModel submit, string packagePath); +} diff --git a/src/LiquidCode.Tester.Gateway/Services/PackageDownloadService.cs b/src/LiquidCode.Tester.Gateway/Services/PackageDownloadService.cs new file mode 100644 index 0000000..5abab22 --- /dev/null +++ b/src/LiquidCode.Tester.Gateway/Services/PackageDownloadService.cs @@ -0,0 +1,49 @@ +namespace LiquidCode.Tester.Gateway.Services; + +public class PackageDownloadService : IPackageDownloadService +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + private readonly string _downloadDirectory; + + public PackageDownloadService( + IHttpClientFactory httpClientFactory, + ILogger logger, + IConfiguration configuration) + { + _httpClientFactory = httpClientFactory; + _logger = logger; + _downloadDirectory = configuration["PackageDownloadDirectory"] ?? Path.Combine(Path.GetTempPath(), "packages"); + + if (!Directory.Exists(_downloadDirectory)) + { + Directory.CreateDirectory(_downloadDirectory); + } + } + + public async Task DownloadPackageAsync(string packageUrl) + { + _logger.LogInformation("Downloading package from {Url}", packageUrl); + + try + { + var httpClient = _httpClientFactory.CreateClient(); + var response = await httpClient.GetAsync(packageUrl); + response.EnsureSuccessStatusCode(); + + var fileName = $"package_{Guid.NewGuid()}.zip"; + var filePath = Path.Combine(_downloadDirectory, fileName); + + await using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None); + await response.Content.CopyToAsync(fileStream); + + _logger.LogInformation("Package downloaded successfully to {Path}", filePath); + return filePath; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to download package from {Url}", packageUrl); + throw; + } + } +} diff --git a/src/LiquidCode.Tester.Gateway/Services/WorkerClientService.cs b/src/LiquidCode.Tester.Gateway/Services/WorkerClientService.cs new file mode 100644 index 0000000..41da9ab --- /dev/null +++ b/src/LiquidCode.Tester.Gateway/Services/WorkerClientService.cs @@ -0,0 +1,92 @@ +using System.Net.Http.Headers; +using LiquidCode.Tester.Common.Models; + +namespace LiquidCode.Tester.Gateway.Services; + +public class WorkerClientService : IWorkerClientService +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + + public WorkerClientService( + IHttpClientFactory httpClientFactory, + ILogger logger, + IConfiguration configuration) + { + _httpClientFactory = httpClientFactory; + _logger = logger; + _configuration = configuration; + } + + public async Task SendToWorkerAsync(SubmitForTesterModel submit, string packagePath) + { + var workerUrl = GetWorkerUrlForLanguage(submit.Language); + _logger.LogInformation("Sending submit {SubmitId} to worker at {WorkerUrl}", submit.Id, workerUrl); + + try + { + var httpClient = _httpClientFactory.CreateClient(); + + using var form = new MultipartFormDataContent(); + + // Add submit metadata + form.Add(new StringContent(submit.Id.ToString()), "Id"); + form.Add(new StringContent(submit.MissionId.ToString()), "MissionId"); + form.Add(new StringContent(submit.Language), "Language"); + form.Add(new StringContent(submit.LanguageVersion), "LanguageVersion"); + form.Add(new StringContent(submit.SourceCode), "SourceCode"); + form.Add(new StringContent(submit.CallbackUrl), "CallbackUrl"); + + // Add package file + var fileStream = File.OpenRead(packagePath); + var fileContent = new StreamContent(fileStream); + fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip"); + form.Add(fileContent, "Package", Path.GetFileName(packagePath)); + + var response = await httpClient.PostAsync($"{workerUrl}/api/test", form); + response.EnsureSuccessStatusCode(); + + _logger.LogInformation("Submit {SubmitId} sent successfully to worker", submit.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send submit {SubmitId} to worker", submit.Id); + throw; + } + finally + { + // Clean up downloaded package + try + { + if (File.Exists(packagePath)) + { + File.Delete(packagePath); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to delete package file {Path}", packagePath); + } + } + } + + private string GetWorkerUrlForLanguage(string language) + { + var workerUrl = language.ToLowerInvariant() switch + { + "c++" => _configuration["Workers:Cpp"], + "java" => _configuration["Workers:Java"], + "kotlin" => _configuration["Workers:Kotlin"], + "c#" => _configuration["Workers:CSharp"], + _ => throw new NotSupportedException($"Language {language} is not supported") + }; + + if (string.IsNullOrEmpty(workerUrl)) + { + throw new InvalidOperationException($"Worker URL for language {language} is not configured"); + } + + return workerUrl; + } +} diff --git a/src/LiquidCode.Tester.Gateway/appsettings.Development.json b/src/LiquidCode.Tester.Gateway/appsettings.Development.json index 0c208ae..495e353 100644 --- a/src/LiquidCode.Tester.Gateway/appsettings.Development.json +++ b/src/LiquidCode.Tester.Gateway/appsettings.Development.json @@ -4,5 +4,9 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "PackageDownloadDirectory": "C:\\temp\\packages", + "Workers": { + "Cpp": "http://localhost:8081" } } diff --git a/src/LiquidCode.Tester.Gateway/appsettings.json b/src/LiquidCode.Tester.Gateway/appsettings.json index 10f68b8..3f0f92b 100644 --- a/src/LiquidCode.Tester.Gateway/appsettings.json +++ b/src/LiquidCode.Tester.Gateway/appsettings.json @@ -5,5 +5,12 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "PackageDownloadDirectory": "/tmp/packages", + "Workers": { + "Cpp": "http://liquidcode-tester-worker-cpp:8080", + "Java": "http://liquidcode-tester-worker-java:8080", + "Kotlin": "http://liquidcode-tester-worker-kotlin:8080", + "CSharp": "http://liquidcode-tester-worker-csharp:8080" + } } diff --git a/src/LiquidCode.Tester.Worker/LiquidCode.Tester.Worker.csproj b/src/LiquidCode.Tester.Worker/LiquidCode.Tester.Worker.csproj index e8c41ec..a993745 100644 --- a/src/LiquidCode.Tester.Worker/LiquidCode.Tester.Worker.csproj +++ b/src/LiquidCode.Tester.Worker/LiquidCode.Tester.Worker.csproj @@ -1,13 +1,20 @@ - + - Exe net9.0 enable enable Linux + + + + + + + + .dockerignore