255 lines
8.8 KiB
C#
255 lines
8.8 KiB
C#
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.SignalR;
|
|
using Microsoft.Extensions.Logging;
|
|
using RobotNet10.NavigationTune.Data;
|
|
using RobotNet10.NavigationTune.Hubs;
|
|
using RobotNet10.NavigationTune.Services;
|
|
using RobotNet10.NavigationTune.Shared.Interfaces;
|
|
using RobotNet10.NavigationTune.Shared.Models;
|
|
|
|
namespace RobotNet10.NavigationTune.Controllers;
|
|
|
|
/// <summary>
|
|
/// REST API controller for test execution and control
|
|
/// </summary>
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
public class TuningController(
|
|
ITuningOrchestrator orchestrator,
|
|
IParameterManager parameterManager,
|
|
IScenarioRepository scenarioRepository,
|
|
ILogger<TuningController> logger) : ControllerBase
|
|
{
|
|
|
|
/// <summary>
|
|
/// Execute a single test
|
|
/// </summary>
|
|
[HttpPost("execute")]
|
|
public async Task<ActionResult<TestExecutionResult>> ExecuteTest(
|
|
[FromBody] ExecuteTestRequest request,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
// Load scenario and parameter set
|
|
var scenario = await scenarioRepository.GetByIdAsync(request.ScenarioId);
|
|
if (scenario == null)
|
|
return NotFound(new { error = $"Scenario with ID {request.ScenarioId} not found" });
|
|
|
|
var parameterSet = await parameterManager.GetByIdAsync(request.ParameterSetId);
|
|
if (parameterSet == null)
|
|
return NotFound(new { error = $"Parameter set with ID {request.ParameterSetId} not found" });
|
|
|
|
// Get connection ID from request headers or query
|
|
var connectionId = Request.Headers["X-Connection-Id"].FirstOrDefault()
|
|
?? Request.Query["connectionId"].FirstOrDefault();
|
|
|
|
// Start test and return immediately so UI can enable Stop/Pause; completion is sent via SignalR (ReceiveTestResult)
|
|
var result = await orchestrator.StartTestAsync(
|
|
scenario,
|
|
parameterSet,
|
|
connectionId,
|
|
request.TestRunId,
|
|
cancellationToken);
|
|
|
|
return Ok(result);
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
logger.LogWarning(ex, "Invalid operation executing test");
|
|
return BadRequest(new { error = ex.Message });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "Error executing test");
|
|
return StatusCode(500, new { error = "Failed to execute test", details = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Run batch tests with multiple scenarios
|
|
/// </summary>
|
|
[HttpPost("batch")]
|
|
public async Task<ActionResult<BatchTestResult>> RunBatchTests(
|
|
[FromBody] BatchTestRequest request,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
// Load parameter set
|
|
var parameterSet = await parameterManager.GetByIdAsync(request.ParameterSetId);
|
|
if (parameterSet == null)
|
|
return NotFound(new { error = $"Parameter set with ID {request.ParameterSetId} not found" });
|
|
|
|
// Load scenarios
|
|
var scenarios = new List<TestScenario>();
|
|
foreach (var scenarioId in request.ScenarioIds)
|
|
{
|
|
var scenario = await scenarioRepository.GetByIdAsync(scenarioId);
|
|
if (scenario == null)
|
|
{
|
|
logger.LogWarning("Scenario {ScenarioId} not found, skipping", scenarioId);
|
|
continue;
|
|
}
|
|
scenarios.Add(scenario);
|
|
}
|
|
|
|
if (scenarios.Count == 0)
|
|
return BadRequest(new { error = "No valid scenarios found" });
|
|
|
|
// Execute batch
|
|
var result = await orchestrator.RunBatchTestsAsync(scenarios, parameterSet, cancellationToken);
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "Error running batch tests");
|
|
return StatusCode(500, new { error = "Failed to run batch tests", details = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compare multiple parameter configurations
|
|
/// </summary>
|
|
[HttpPost("compare")]
|
|
public async Task<ActionResult<ComparisonResult>> CompareConfigurations(
|
|
[FromBody] CompareConfigurationsRequest request,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
// Load scenario
|
|
var scenario = await scenarioRepository.GetByIdAsync(request.ScenarioId);
|
|
if (scenario == null)
|
|
return NotFound(new { error = $"Scenario with ID {request.ScenarioId} not found" });
|
|
|
|
// Load parameter sets
|
|
var parameterSets = new List<NavigationParameterSet>();
|
|
foreach (var parameterSetId in request.ParameterSetIds)
|
|
{
|
|
var parameterSet = await parameterManager.GetByIdAsync(parameterSetId);
|
|
if (parameterSet == null)
|
|
{
|
|
logger.LogWarning("Parameter set {ParameterSetId} not found, skipping", parameterSetId);
|
|
continue;
|
|
}
|
|
parameterSets.Add(parameterSet);
|
|
}
|
|
|
|
if (parameterSets.Count == 0)
|
|
return BadRequest(new { error = "No valid parameter sets found" });
|
|
|
|
// Compare
|
|
var result = await orchestrator.CompareConfigurationsAsync(parameterSets, scenario, cancellationToken);
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "Error comparing configurations");
|
|
return StatusCode(500, new { error = "Failed to compare configurations", details = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pause running test
|
|
/// </summary>
|
|
[HttpPost("pause/{testRunId}")]
|
|
public ActionResult PauseTest(string testRunId)
|
|
{
|
|
try
|
|
{
|
|
orchestrator.PauseTest(testRunId);
|
|
return Ok(new { message = "Test paused" });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "Error pausing test {TestRunId}", testRunId);
|
|
return StatusCode(500, new { error = "Failed to pause test" });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resume paused test
|
|
/// </summary>
|
|
[HttpPost("resume/{testRunId}")]
|
|
public ActionResult ResumeTest(string testRunId)
|
|
{
|
|
try
|
|
{
|
|
orchestrator.ResumeTest(testRunId);
|
|
return Ok(new { message = "Test resumed" });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "Error resuming test {TestRunId}", testRunId);
|
|
return StatusCode(500, new { error = "Failed to resume test" });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stop running test
|
|
/// </summary>
|
|
[HttpPost("stop/{testRunId}")]
|
|
public ActionResult StopTest(string testRunId)
|
|
{
|
|
try
|
|
{
|
|
orchestrator.StopTest(testRunId);
|
|
return Ok(new { message = "Test stopped" });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "Error stopping test {TestRunId}", testRunId);
|
|
return StatusCode(500, new { error = "Failed to stop test" });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emergency stop
|
|
/// </summary>
|
|
[HttpPost("emergency-stop/{testRunId}")]
|
|
public ActionResult EmergencyStop(string testRunId)
|
|
{
|
|
try
|
|
{
|
|
orchestrator.EmergencyStop(testRunId);
|
|
return Ok(new { message = "Emergency stop activated" });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "Error emergency stopping test {TestRunId}", testRunId);
|
|
return StatusCode(500, new { error = "Failed to emergency stop test" });
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request model for executing a single test.
|
|
/// When TestRunId is provided, client should call JoinTestSession(TestRunId) before execute so real-time telemetry is received.
|
|
/// </summary>
|
|
public class ExecuteTestRequest
|
|
{
|
|
public Guid ScenarioId { get; set; }
|
|
public Guid ParameterSetId { get; set; }
|
|
/// <summary>Optional. If set, this Id is used for the test run and for SignalR group test_{TestRunId}.</summary>
|
|
public Guid? TestRunId { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request model for batch test execution
|
|
/// </summary>
|
|
public class BatchTestRequest
|
|
{
|
|
public List<Guid> ScenarioIds { get; set; } = new();
|
|
public Guid ParameterSetId { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request model for configuration comparison
|
|
/// </summary>
|
|
public class CompareConfigurationsRequest
|
|
{
|
|
public Guid ScenarioId { get; set; }
|
|
public List<Guid> ParameterSetIds { get; set; } = new();
|
|
}
|