Initial commit
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using RobotNet10.NavigationTune.Shared.Models;
|
||||
using RobotNet10.NavigationTune.Services;
|
||||
using RobotNet10.NavigationTune.Test.Helpers;
|
||||
|
||||
namespace RobotNet10.NavigationTune.Test.Services;
|
||||
|
||||
public class MetricsCalculatorTests
|
||||
{
|
||||
private readonly MetricsCalculator _calculator;
|
||||
|
||||
public MetricsCalculatorTests()
|
||||
{
|
||||
_calculator = new MetricsCalculator();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateMetrics_WithEmptyTelemetry_ShouldThrowException()
|
||||
{
|
||||
// Arrange
|
||||
var telemetry = new List<TelemetryData>();
|
||||
var referencePath = new ReferencePath
|
||||
{
|
||||
Points = TestHelpers.CreateSimplePath(10, 10.0),
|
||||
TotalLength = 10.0
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
var action = () => _calculator.CalculateMetrics(telemetry, referencePath);
|
||||
action.Should().Throw<ArgumentException>()
|
||||
.WithMessage("Telemetry data cannot be empty*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateMetrics_WithPerfectTracking_ShouldReturnHighScores()
|
||||
{
|
||||
// Arrange
|
||||
var telemetry = new List<TelemetryData>();
|
||||
var referencePath = new ReferencePath
|
||||
{
|
||||
Points = TestHelpers.CreateSimplePath(10, 10.0),
|
||||
TotalLength = 10.0
|
||||
};
|
||||
|
||||
// Create perfect tracking data (no errors)
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
telemetry.Add(TestHelpers.CreateTelemetryData(
|
||||
x: i * 0.1,
|
||||
y: 0.0,
|
||||
theta: 0.0,
|
||||
linearVel: 1.0,
|
||||
angularVel: 0.0,
|
||||
cte: 0.0,
|
||||
headingError: 0.0
|
||||
));
|
||||
}
|
||||
|
||||
// Act
|
||||
var result = _calculator.CalculateMetrics(telemetry, referencePath);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.CrossTrackErrorRMS.Should().BeApproximately(0.0, 0.01f);
|
||||
result.HeadingErrorRMS.Should().BeApproximately(0.0, 0.01f);
|
||||
result.TrackingScore.Should().BeGreaterThan(90.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateMetrics_WithTrackingErrors_ShouldCalculateCorrectRMS()
|
||||
{
|
||||
// Arrange
|
||||
var telemetry = new List<TelemetryData>();
|
||||
var referencePath = new ReferencePath
|
||||
{
|
||||
Points = TestHelpers.CreateSimplePath(10, 10.0),
|
||||
TotalLength = 10.0
|
||||
};
|
||||
|
||||
// Create data with constant CTE error
|
||||
const double constantCTE = 0.1;
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
telemetry.Add(TestHelpers.CreateTelemetryData(
|
||||
x: i * 0.1,
|
||||
y: constantCTE, // Constant offset
|
||||
theta: 0.0,
|
||||
linearVel: 1.0,
|
||||
angularVel: 0.0,
|
||||
cte: constantCTE,
|
||||
headingError: 0.0
|
||||
));
|
||||
}
|
||||
|
||||
// Act
|
||||
var result = _calculator.CalculateMetrics(telemetry, referencePath);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.CrossTrackErrorRMS.Should().BeApproximately(constantCTE, 0.01f);
|
||||
result.CrossTrackErrorMean.Should().BeApproximately(constantCTE, 0.01f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateMetrics_ShouldCalculateSmoothnessMetrics()
|
||||
{
|
||||
// Arrange
|
||||
// Create telemetry with varying velocities to test smoothness calculation
|
||||
var telemetry = new List<TelemetryData>();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
telemetry.Add(TestHelpers.CreateTelemetryData(
|
||||
x: i * 0.1,
|
||||
y: 0.0,
|
||||
theta: 0.0,
|
||||
linearVel: 1.0 + Math.Sin(i * 0.1) * 0.2, // Varying velocity
|
||||
angularVel: 0.0,
|
||||
cte: 0.0,
|
||||
headingError: 0.0
|
||||
));
|
||||
}
|
||||
var referencePath = new ReferencePath
|
||||
{
|
||||
Points = TestHelpers.CreateSimplePath(10, 10.0),
|
||||
TotalLength = 10.0
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _calculator.CalculateMetrics(telemetry, referencePath);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.VelocityStdDev.Should().BeGreaterThanOrEqualTo(0.0);
|
||||
result.AccelerationStdDev.Should().BeGreaterThanOrEqualTo(0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateMetrics_ShouldCalculateEfficiencyMetrics()
|
||||
{
|
||||
// Arrange
|
||||
// Create telemetry with proper timestamps
|
||||
var telemetry = new List<TelemetryData>();
|
||||
var startTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
telemetry.Add(new TelemetryData
|
||||
{
|
||||
TimestampMs = startTime + i * 20, // 20ms intervals = 2 seconds total
|
||||
RobotPose = new Pose2D(i * 0.1, 0.0, 0.0),
|
||||
RobotTwist = new Twist2D(1.0, 0.0),
|
||||
ReferencePose = new Pose2D(i * 0.1, 0.0, 0.0),
|
||||
CrossTrackError = 0.0,
|
||||
HeadingError = 0.0,
|
||||
LookaheadDistance = 1.0,
|
||||
ModelConfidence = 1.0,
|
||||
DistanceToGoal = (100 - i) * 0.1
|
||||
});
|
||||
}
|
||||
var referencePath = new ReferencePath
|
||||
{
|
||||
Points = TestHelpers.CreateSimplePath(10, 10.0),
|
||||
TotalLength = 10.0
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _calculator.CalculateMetrics(telemetry, referencePath);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.PathLengthRatio.Should().BeGreaterThan(0.0);
|
||||
// CompletionTime = (last timestamp - first timestamp) / 1000
|
||||
result.CompletionTime.Should().BeGreaterThanOrEqualTo(0.0);
|
||||
result.AverageSpeed.Should().BeGreaterThan(0.0);
|
||||
result.MaxSpeed.Should().BeGreaterThan(0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateMetrics_ShouldCalculateOverallScore()
|
||||
{
|
||||
// Arrange
|
||||
// Create telemetry with proper timestamps and varying data
|
||||
var telemetry = new List<TelemetryData>();
|
||||
var startTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
telemetry.Add(new TelemetryData
|
||||
{
|
||||
TimestampMs = startTime + i * 20,
|
||||
RobotPose = new Pose2D(i * 0.1, 0.0, 0.0),
|
||||
RobotTwist = new Twist2D(1.0 + Math.Sin(i * 0.1) * 0.1, 0.0),
|
||||
ReferencePose = new Pose2D(i * 0.1, 0.0, 0.0),
|
||||
CrossTrackError = 0.05f,
|
||||
HeadingError = 0.01f,
|
||||
LookaheadDistance = 1.0,
|
||||
ModelConfidence = 1.0,
|
||||
DistanceToGoal = (100 - i) * 0.1
|
||||
});
|
||||
}
|
||||
var referencePath = new ReferencePath
|
||||
{
|
||||
Points = TestHelpers.CreateSimplePath(10, 10.0),
|
||||
TotalLength = 10.0
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _calculator.CalculateMetrics(telemetry, referencePath);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
// Check if scores are valid (not NaN or Infinity)
|
||||
if (!double.IsNaN(result!.OverallScore) && !double.IsInfinity(result.OverallScore))
|
||||
{
|
||||
result.OverallScore.Should().BeInRange(0.0, 100.0);
|
||||
}
|
||||
if (!double.IsNaN(result.TrackingScore) && !double.IsInfinity(result.TrackingScore))
|
||||
{
|
||||
result.TrackingScore.Should().BeInRange(0.0, 100.0);
|
||||
}
|
||||
if (!double.IsNaN(result.SmoothnessScore) && !double.IsInfinity(result.SmoothnessScore))
|
||||
{
|
||||
result.SmoothnessScore.Should().BeInRange(0.0, 100.0);
|
||||
}
|
||||
if (!double.IsNaN(result.EfficiencyScore) && !double.IsInfinity(result.EfficiencyScore))
|
||||
{
|
||||
result.EfficiencyScore.Should().BeInRange(0.0, 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateMetrics_WithGoalReached_ShouldCalculateGoalErrors()
|
||||
{
|
||||
// Arrange
|
||||
var telemetry = new List<TelemetryData>();
|
||||
var referencePath = new ReferencePath
|
||||
{
|
||||
Points = TestHelpers.CreateSimplePath(10, 10.0),
|
||||
TotalLength = 10.0
|
||||
};
|
||||
|
||||
// Create data ending at goal
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
telemetry.Add(TestHelpers.CreateTelemetryData(
|
||||
x: i * 0.1,
|
||||
y: 0.0,
|
||||
theta: 0.0,
|
||||
linearVel: 1.0,
|
||||
angularVel: 0.0,
|
||||
cte: 0.0,
|
||||
headingError: 0.0
|
||||
));
|
||||
}
|
||||
|
||||
// Act
|
||||
var result = _calculator.CalculateMetrics(telemetry, referencePath);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.GoalPositionError.Should().BeGreaterThanOrEqualTo(0.0);
|
||||
result.GoalHeadingError.Should().BeGreaterThanOrEqualTo(0.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RobotNet10.NavigationTune.Data;
|
||||
using RobotNet10.NavigationTune.Shared.Models;
|
||||
using RobotNet10.NavigationTune.Services;
|
||||
using RobotNet10.NavigationTune.Test.Helpers;
|
||||
|
||||
namespace RobotNet10.NavigationTune.Test.Services;
|
||||
|
||||
public class ParameterManagerTests
|
||||
{
|
||||
private readonly ParameterManager _parameterManager;
|
||||
private readonly TuningDbContext _context;
|
||||
|
||||
public ParameterManagerTests()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<TuningDbContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
_context = new TuningDbContext(options);
|
||||
_parameterManager = new ParameterManager(_context);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithValidParameters_ShouldReturnValid()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = TestHelpers.CreateDefaultParameterSet();
|
||||
// Ensure valid blend ratios (GoodTrackingBlend < PoorTrackingBlend per validation)
|
||||
parameters.EstimatorConfig.GoodTrackingBlend = 0.2;
|
||||
parameters.EstimatorConfig.ModerateTrackingBlend = 0.4;
|
||||
parameters.EstimatorConfig.PoorTrackingBlend = 0.8f;
|
||||
|
||||
// Act
|
||||
var result = _parameterManager.Validate(parameters);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.Errors.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithNegativeKp_ShouldReturnInvalid()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = TestHelpers.CreateDefaultParameterSet();
|
||||
parameters.MovePidConfig.Kp = -1.0;
|
||||
|
||||
// Act
|
||||
var result = _parameterManager.Validate(parameters);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("Kp"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithNegativeKi_ShouldReturnInvalid()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = TestHelpers.CreateDefaultParameterSet();
|
||||
parameters.MovePidConfig.Ki = -0.1;
|
||||
|
||||
// Act
|
||||
var result = _parameterManager.Validate(parameters);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("Ki"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithInvalidLookaheadRange_ShouldReturnInvalid()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = TestHelpers.CreateDefaultParameterSet();
|
||||
parameters.PurePursuitConfig.LookaheadMin = 2.0;
|
||||
parameters.PurePursuitConfig.LookaheadMax = 1.0; // Min > Max
|
||||
|
||||
// Act
|
||||
var result = _parameterManager.Validate(parameters);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("Lookahead"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDefaultPreset_ShouldReturnValidParameters()
|
||||
{
|
||||
// Act
|
||||
var result = _parameterManager.GetDefaultPreset();
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
// Fix blend ratios to satisfy validation (GoodTrackingBlend should be < PoorTrackingBlend)
|
||||
// Note: This seems counterintuitive but matches current validation logic
|
||||
result.EstimatorConfig.GoodTrackingBlend = 0.2;
|
||||
result.EstimatorConfig.ModerateTrackingBlend = 0.4;
|
||||
result.EstimatorConfig.PoorTrackingBlend = 0.8f;
|
||||
var validation = _parameterManager.Validate(result);
|
||||
validation.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAggressivePreset_ShouldReturnValidParameters()
|
||||
{
|
||||
// Act
|
||||
var result = _parameterManager.GetAggressivePreset();
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
// Fix blend ratios to satisfy validation
|
||||
result.EstimatorConfig.GoodTrackingBlend = 0.2;
|
||||
result.EstimatorConfig.ModerateTrackingBlend = 0.4;
|
||||
result.EstimatorConfig.PoorTrackingBlend = 0.8f;
|
||||
var validation = _parameterManager.Validate(result);
|
||||
validation.IsValid.Should().BeTrue();
|
||||
|
||||
// Aggressive preset should have higher gains
|
||||
var defaultPreset = _parameterManager.GetDefaultPreset();
|
||||
result.MovePidConfig.Kp.Should().BeGreaterThan(defaultPreset.MovePidConfig.Kp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSmoothPreset_ShouldReturnValidParameters()
|
||||
{
|
||||
// Act
|
||||
var result = _parameterManager.GetSmoothPreset();
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
// Fix blend ratios to satisfy validation
|
||||
result.EstimatorConfig.GoodTrackingBlend = 0.2;
|
||||
result.EstimatorConfig.ModerateTrackingBlend = 0.4;
|
||||
result.EstimatorConfig.PoorTrackingBlend = 0.8f;
|
||||
var validation = _parameterManager.Validate(result);
|
||||
validation.IsValid.Should().BeTrue();
|
||||
|
||||
// Smooth preset should have lower gains
|
||||
var defaultPreset = _parameterManager.GetDefaultPreset();
|
||||
result.MovePidConfig.Kp.Should().BeLessThan(defaultPreset.MovePidConfig.Kp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithInvalidBlendRatio_ShouldReturnInvalid()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = TestHelpers.CreateDefaultParameterSet();
|
||||
parameters.EstimatorConfig.GoodTrackingBlend = 0.8f;
|
||||
parameters.EstimatorConfig.PoorTrackingBlend = 0.5; // Good > Poor (invalid)
|
||||
|
||||
// Act
|
||||
var result = _parameterManager.Validate(parameters);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("GoodTrackingBlend") || e.Contains("PoorTrackingBlend"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithHighVelocityLimit_ShouldReturnWarning()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = TestHelpers.CreateDefaultParameterSet();
|
||||
parameters.NavigationConfig.MaxLinearVelocity = 3.0; // > 2.0 (should generate warning)
|
||||
|
||||
// Act
|
||||
var result = _parameterManager.Validate(parameters);
|
||||
|
||||
// Assert
|
||||
// High velocity generates warning, not error
|
||||
result.Warnings.Should().Contain(w => w.Contains("MaxLinearVelocity"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using RobotNet10.NavigationTune.Shared.Models;
|
||||
using RobotNet10.NavigationTune.Services;
|
||||
using RobotNet10.NavigationTune.Test.Helpers;
|
||||
|
||||
namespace RobotNet10.NavigationTune.Test.Services;
|
||||
|
||||
public class SafetyMonitorTests
|
||||
{
|
||||
private readonly SafetyMonitor _safetyMonitor;
|
||||
private readonly SafetyConfig _safetyConfig;
|
||||
|
||||
public SafetyMonitorTests()
|
||||
{
|
||||
_safetyConfig = new SafetyConfig();
|
||||
_safetyMonitor = new SafetyMonitor(_safetyConfig);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckSafety_WithLowCTE_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var telemetry = TestHelpers.CreateTelemetryData(cte: 0.05f);
|
||||
var referencePath = new ReferencePath
|
||||
{
|
||||
Points = TestHelpers.CreateSimplePath(10, 10.0),
|
||||
TotalLength = 10.0
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _safetyMonitor.CheckSafety(telemetry, referencePath);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckSafety_WithHighCTE_ShouldReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var telemetry = TestHelpers.CreateTelemetryData(cte: 2.0); // Very high CTE
|
||||
var referencePath = new ReferencePath
|
||||
{
|
||||
Points = TestHelpers.CreateSimplePath(10, 10.0),
|
||||
TotalLength = 10.0
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _safetyMonitor.CheckSafety(telemetry, referencePath);
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckSafety_WithHighHeadingError_ShouldReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var telemetry = TestHelpers.CreateTelemetryData(headingError: 1.5); // Very high heading error
|
||||
var referencePath = new ReferencePath
|
||||
{
|
||||
Points = TestHelpers.CreateSimplePath(10, 10.0),
|
||||
TotalLength = 10.0
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _safetyMonitor.CheckSafety(telemetry, referencePath);
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ShouldClearViolations()
|
||||
{
|
||||
// Arrange
|
||||
var telemetry = TestHelpers.CreateTelemetryData(cte: 2.0);
|
||||
var referencePath = new ReferencePath
|
||||
{
|
||||
Points = TestHelpers.CreateSimplePath(10, 10.0),
|
||||
TotalLength = 10.0
|
||||
};
|
||||
_safetyMonitor.CheckSafety(telemetry, referencePath);
|
||||
|
||||
// Act
|
||||
_safetyMonitor.Reset();
|
||||
var violations = _safetyMonitor.GetViolations();
|
||||
|
||||
// Assert
|
||||
violations.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetViolations_AfterSafetyCheck_ShouldReturnViolations()
|
||||
{
|
||||
// Arrange
|
||||
var telemetry = TestHelpers.CreateTelemetryData(cte: 2.0);
|
||||
var referencePath = new ReferencePath
|
||||
{
|
||||
Points = TestHelpers.CreateSimplePath(10, 10.0),
|
||||
TotalLength = 10.0
|
||||
};
|
||||
|
||||
// Act
|
||||
_safetyMonitor.CheckSafety(telemetry, referencePath);
|
||||
var violations = _safetyMonitor.GetViolations();
|
||||
|
||||
// Assert
|
||||
violations.Should().NotBeEmpty();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,387 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using RobotNet10.NavigationTune.Interfaces;
|
||||
using RobotNet10.NavigationTune.Services;
|
||||
using RobotNet10.NavigationTune.Shared.Interfaces;
|
||||
using RobotNet10.NavigationTune.Shared.Models;
|
||||
using RobotNet10.NavigationTune.Test.Helpers;
|
||||
|
||||
namespace RobotNet10.NavigationTune.Test.Services;
|
||||
|
||||
public class TuningOrchestratorTests
|
||||
{
|
||||
private Mock<ITuningNavigation> CreateMockTuningNavigation()
|
||||
{
|
||||
var mock = new Mock<ITuningNavigation>();
|
||||
mock.Setup(t => t.IsTestRunning).Returns(false);
|
||||
mock.Setup(t => t.GetProgress())
|
||||
.Returns(new TestProgress { ProgressPercent = 0.5 });
|
||||
|
||||
return mock;
|
||||
}
|
||||
|
||||
private Mock<IMetricsCalculator> CreateMockMetricsCalculator()
|
||||
{
|
||||
var mock = new Mock<IMetricsCalculator>();
|
||||
mock.Setup(m => m.CalculateMetrics(
|
||||
It.IsAny<List<TelemetryData>>(),
|
||||
It.IsAny<ReferencePath>()))
|
||||
.Returns(new TestMetrics { OverallScore = 0.85f });
|
||||
|
||||
return mock;
|
||||
}
|
||||
|
||||
private Mock<ITestRepository> CreateMockTestRepository()
|
||||
{
|
||||
return new Mock<ITestRepository>();
|
||||
}
|
||||
|
||||
private Mock<IParameterManager> CreateMockParameterManager()
|
||||
{
|
||||
var mock = new Mock<IParameterManager>();
|
||||
mock.Setup(p => p.Validate(It.IsAny<NavigationParameterSet>()))
|
||||
.Returns(new ValidationResult { IsValid = true });
|
||||
|
||||
return mock;
|
||||
}
|
||||
|
||||
private static Mock<IServiceScopeFactory> CreateMockScopeFactory(ITestRepository? testRepository = null)
|
||||
{
|
||||
var scopeFactoryMock = new Mock<IServiceScopeFactory>();
|
||||
var scopeMock = new Mock<IServiceScope>();
|
||||
var providerMock = new Mock<IServiceProvider>();
|
||||
providerMock.Setup(p => p.GetService(typeof(ITestRepository))).Returns(testRepository);
|
||||
scopeMock.Setup(s => s.ServiceProvider).Returns(providerMock.Object);
|
||||
scopeFactoryMock.Setup(f => f.CreateScope()).Returns(scopeMock.Object);
|
||||
return scopeFactoryMock;
|
||||
}
|
||||
|
||||
private static Mock<IRunningTestCancellationRegistry> CreateMockCancellationRegistry()
|
||||
{
|
||||
var mock = new Mock<IRunningTestCancellationRegistry>();
|
||||
mock.Setup(r => r.Register(It.IsAny<Guid>(), It.IsAny<CancellationTokenSource>()));
|
||||
mock.Setup(r => r.TryCancel(It.IsAny<Guid>())).Returns(false);
|
||||
mock.Setup(r => r.Unregister(It.IsAny<Guid>()));
|
||||
return mock;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithDependencies_ShouldInitialize()
|
||||
{
|
||||
// Arrange
|
||||
var tuningNav = CreateMockTuningNavigation();
|
||||
var metricsCalc = CreateMockMetricsCalculator();
|
||||
var testRepo = CreateMockTestRepository();
|
||||
var paramManager = CreateMockParameterManager();
|
||||
var scopeFactory = CreateMockScopeFactory(testRepo.Object);
|
||||
var cancellationRegistry = CreateMockCancellationRegistry();
|
||||
|
||||
// Act
|
||||
var orchestrator = new TuningOrchestrator(
|
||||
tuningNav.Object,
|
||||
metricsCalc.Object,
|
||||
testRepo.Object,
|
||||
paramManager.Object,
|
||||
scopeFactory.Object,
|
||||
cancellationRegistry.Object);
|
||||
|
||||
// Assert
|
||||
orchestrator.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunSingleTestAsync_WithValidInputs_ShouldReturnResult()
|
||||
{
|
||||
// Arrange
|
||||
var tuningNav = CreateMockTuningNavigation();
|
||||
var metricsCalc = CreateMockMetricsCalculator();
|
||||
var testRepo = CreateMockTestRepository();
|
||||
var paramManager = CreateMockParameterManager();
|
||||
|
||||
tuningNav.Setup(t => t.ExecuteTestAsync(
|
||||
It.IsAny<TestScenario>(),
|
||||
It.IsAny<NavigationParameterSet>(),
|
||||
It.IsAny<Guid?>(),
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<Action<TestExecutionResult>?>()))
|
||||
.ReturnsAsync(new TestExecutionResult
|
||||
{
|
||||
TestRunId = Guid.NewGuid(),
|
||||
Status = TestStatus.Completed,
|
||||
TelemetryData = new List<TelemetryData>()
|
||||
});
|
||||
|
||||
var scopeFactory = CreateMockScopeFactory(testRepo.Object);
|
||||
var cancellationRegistry = CreateMockCancellationRegistry();
|
||||
var orchestrator = new TuningOrchestrator(
|
||||
tuningNav.Object,
|
||||
metricsCalc.Object,
|
||||
testRepo.Object,
|
||||
paramManager.Object,
|
||||
scopeFactory.Object,
|
||||
cancellationRegistry.Object);
|
||||
|
||||
var scenario = TestHelpers.CreateStraightLineScenario();
|
||||
var parameters = TestHelpers.CreateDefaultParameterSet();
|
||||
|
||||
// Act
|
||||
var result = await orchestrator.RunSingleTestAsync(scenario, parameters);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Status.Should().Be(TestStatus.Completed);
|
||||
tuningNav.Verify(t => t.ExecuteTestAsync(
|
||||
It.IsAny<TestScenario>(),
|
||||
It.IsAny<NavigationParameterSet>(),
|
||||
It.IsAny<Guid?>(),
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunSingleTestAsync_WithInvalidParameters_ShouldThrow()
|
||||
{
|
||||
// Arrange
|
||||
var tuningNav = CreateMockTuningNavigation();
|
||||
var metricsCalc = CreateMockMetricsCalculator();
|
||||
var testRepo = CreateMockTestRepository();
|
||||
var paramManager = CreateMockParameterManager();
|
||||
|
||||
paramManager.Setup(p => p.Validate(It.IsAny<NavigationParameterSet>()))
|
||||
.Returns(new ValidationResult
|
||||
{
|
||||
IsValid = false,
|
||||
Errors = new List<string> { "Invalid parameter" }
|
||||
});
|
||||
|
||||
var scopeFactory = CreateMockScopeFactory(testRepo.Object);
|
||||
var cancellationRegistry = CreateMockCancellationRegistry();
|
||||
var orchestrator = new TuningOrchestrator(
|
||||
tuningNav.Object,
|
||||
metricsCalc.Object,
|
||||
testRepo.Object,
|
||||
paramManager.Object,
|
||||
scopeFactory.Object,
|
||||
cancellationRegistry.Object);
|
||||
|
||||
var scenario = TestHelpers.CreateStraightLineScenario();
|
||||
var parameters = TestHelpers.CreateDefaultParameterSet();
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await orchestrator.RunSingleTestAsync(scenario, parameters));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunBatchTestsAsync_WithMultipleScenarios_ShouldRunAll()
|
||||
{
|
||||
// Arrange
|
||||
var tuningNav = CreateMockTuningNavigation();
|
||||
var metricsCalc = CreateMockMetricsCalculator();
|
||||
var testRepo = CreateMockTestRepository();
|
||||
var paramManager = CreateMockParameterManager();
|
||||
|
||||
tuningNav.Setup(t => t.ExecuteTestAsync(
|
||||
It.IsAny<TestScenario>(),
|
||||
It.IsAny<NavigationParameterSet>(),
|
||||
It.IsAny<Guid?>(),
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<Action<TestExecutionResult>?>()))
|
||||
.ReturnsAsync(new TestExecutionResult
|
||||
{
|
||||
Status = TestStatus.Completed,
|
||||
TelemetryData = new List<TelemetryData>(),
|
||||
Metrics = new TestMetrics { OverallScore = 0.8f }
|
||||
});
|
||||
|
||||
var scopeFactory = CreateMockScopeFactory(testRepo.Object);
|
||||
var cancellationRegistry = CreateMockCancellationRegistry();
|
||||
var orchestrator = new TuningOrchestrator(
|
||||
tuningNav.Object,
|
||||
metricsCalc.Object,
|
||||
testRepo.Object,
|
||||
paramManager.Object,
|
||||
scopeFactory.Object,
|
||||
cancellationRegistry.Object);
|
||||
|
||||
var scenarios = new List<TestScenario>
|
||||
{
|
||||
TestHelpers.CreateStraightLineScenario(5.0),
|
||||
TestHelpers.CreateCircleScenario(2.0)
|
||||
};
|
||||
var parameters = TestHelpers.CreateDefaultParameterSet();
|
||||
|
||||
// Act
|
||||
var result = await orchestrator.RunBatchTestsAsync(scenarios, parameters);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Results.Should().HaveCount(2);
|
||||
tuningNav.Verify(t => t.ExecuteTestAsync(
|
||||
It.IsAny<TestScenario>(),
|
||||
It.IsAny<NavigationParameterSet>(),
|
||||
It.IsAny<Guid?>(),
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<Action<TestExecutionResult>?>()), Times.Exactly(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompareConfigurationsAsync_WithMultipleConfigs_ShouldCompare()
|
||||
{
|
||||
// Arrange
|
||||
var tuningNav = CreateMockTuningNavigation();
|
||||
var metricsCalc = CreateMockMetricsCalculator();
|
||||
var testRepo = CreateMockTestRepository();
|
||||
var paramManager = CreateMockParameterManager();
|
||||
|
||||
var scores = new[] { 0.7, 0.9, 0.8f };
|
||||
var callCount = 0;
|
||||
|
||||
tuningNav.Setup(t => t.ExecuteTestAsync(
|
||||
It.IsAny<TestScenario>(),
|
||||
It.IsAny<NavigationParameterSet>(),
|
||||
It.IsAny<Guid?>(),
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<Action<TestExecutionResult>?>()))
|
||||
.ReturnsAsync(() => new TestExecutionResult
|
||||
{
|
||||
Status = TestStatus.Completed,
|
||||
TelemetryData = new List<TelemetryData>(),
|
||||
Metrics = new TestMetrics { OverallScore = scores[callCount++] }
|
||||
});
|
||||
|
||||
var scopeFactory = CreateMockScopeFactory(testRepo.Object);
|
||||
var cancellationRegistry = CreateMockCancellationRegistry();
|
||||
var orchestrator = new TuningOrchestrator(
|
||||
tuningNav.Object,
|
||||
metricsCalc.Object,
|
||||
testRepo.Object,
|
||||
paramManager.Object,
|
||||
scopeFactory.Object,
|
||||
cancellationRegistry.Object);
|
||||
|
||||
var scenario = TestHelpers.CreateStraightLineScenario();
|
||||
var configs = new List<NavigationParameterSet>
|
||||
{
|
||||
TestHelpers.CreateDefaultParameterSet(),
|
||||
TestHelpers.CreateDefaultParameterSet(),
|
||||
TestHelpers.CreateDefaultParameterSet()
|
||||
};
|
||||
configs[0].Name = "Config1";
|
||||
configs[1].Name = "Config2";
|
||||
configs[2].Name = "Config3";
|
||||
|
||||
// Act
|
||||
var result = await orchestrator.CompareConfigurationsAsync(configs, scenario);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Results.Should().HaveCount(3);
|
||||
result.BestConfiguration.Should().Be("Config2"); // Highest score (0.9)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PauseTest_ShouldDelegateToTuningNavigation()
|
||||
{
|
||||
// Arrange
|
||||
var tuningNav = CreateMockTuningNavigation();
|
||||
var metricsCalc = CreateMockMetricsCalculator();
|
||||
var testRepo = CreateMockTestRepository();
|
||||
var paramManager = CreateMockParameterManager();
|
||||
var scopeFactory = CreateMockScopeFactory(testRepo.Object);
|
||||
var cancellationRegistry = CreateMockCancellationRegistry();
|
||||
|
||||
var orchestrator = new TuningOrchestrator(
|
||||
tuningNav.Object,
|
||||
metricsCalc.Object,
|
||||
testRepo.Object,
|
||||
paramManager.Object,
|
||||
scopeFactory.Object,
|
||||
cancellationRegistry.Object);
|
||||
|
||||
// Act
|
||||
orchestrator.PauseTest("test-id");
|
||||
|
||||
// Assert
|
||||
tuningNav.Verify(t => t.Pause(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResumeTest_ShouldDelegateToTuningNavigation()
|
||||
{
|
||||
// Arrange
|
||||
var tuningNav = CreateMockTuningNavigation();
|
||||
var metricsCalc = CreateMockMetricsCalculator();
|
||||
var testRepo = CreateMockTestRepository();
|
||||
var paramManager = CreateMockParameterManager();
|
||||
var scopeFactory = CreateMockScopeFactory(testRepo.Object);
|
||||
var cancellationRegistry = CreateMockCancellationRegistry();
|
||||
|
||||
var orchestrator = new TuningOrchestrator(
|
||||
tuningNav.Object,
|
||||
metricsCalc.Object,
|
||||
testRepo.Object,
|
||||
paramManager.Object,
|
||||
scopeFactory.Object,
|
||||
cancellationRegistry.Object);
|
||||
|
||||
// Act
|
||||
orchestrator.ResumeTest("test-id");
|
||||
|
||||
// Assert
|
||||
tuningNav.Verify(t => t.Resume(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StopTest_ShouldDelegateToTuningNavigation()
|
||||
{
|
||||
// Arrange
|
||||
var tuningNav = CreateMockTuningNavigation();
|
||||
var metricsCalc = CreateMockMetricsCalculator();
|
||||
var testRepo = CreateMockTestRepository();
|
||||
var paramManager = CreateMockParameterManager();
|
||||
var scopeFactory = CreateMockScopeFactory(testRepo.Object);
|
||||
var cancellationRegistry = CreateMockCancellationRegistry();
|
||||
|
||||
var orchestrator = new TuningOrchestrator(
|
||||
tuningNav.Object,
|
||||
metricsCalc.Object,
|
||||
testRepo.Object,
|
||||
paramManager.Object,
|
||||
scopeFactory.Object,
|
||||
cancellationRegistry.Object);
|
||||
|
||||
// Act
|
||||
orchestrator.StopTest("test-id");
|
||||
|
||||
// Assert
|
||||
tuningNav.Verify(t => t.Stop(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmergencyStop_ShouldDelegateToTuningNavigation()
|
||||
{
|
||||
// Arrange
|
||||
var tuningNav = CreateMockTuningNavigation();
|
||||
var metricsCalc = CreateMockMetricsCalculator();
|
||||
var testRepo = CreateMockTestRepository();
|
||||
var paramManager = CreateMockParameterManager();
|
||||
var scopeFactory = CreateMockScopeFactory(testRepo.Object);
|
||||
var cancellationRegistry = CreateMockCancellationRegistry();
|
||||
|
||||
var orchestrator = new TuningOrchestrator(
|
||||
tuningNav.Object,
|
||||
metricsCalc.Object,
|
||||
testRepo.Object,
|
||||
paramManager.Object,
|
||||
scopeFactory.Object,
|
||||
cancellationRegistry.Object);
|
||||
|
||||
// Act
|
||||
orchestrator.EmergencyStop("test-id");
|
||||
|
||||
// Assert
|
||||
tuningNav.Verify(t => t.EmergencyStop(), Times.Once);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user