mirror of
https://github.com/ivuorinen/hiha-arvio.git
synced 2026-01-26 03:14:00 +00:00
Implemented platform-agnostic accelerometer abstraction with iOS and desktop implementations: **IAccelerometerService Interface:** - Platform-agnostic abstraction for sensor input - SensorReading model (renamed from AccelerometerData to avoid MAUI conflict) - X, Y, Z acceleration values in g-force units - ReadingChanged event for continuous data stream - Start/Stop methods for sensor control - IsSupported property for platform capability detection **IosAccelerometerService (10 tests):** - Uses MAUI's Microsoft.Maui.Devices.Sensors.Accelerometer - Works on iOS devices and simulator (provides simulated data) - Conditional compilation (#if IOS || MACCATALYST) - Converts MAUI AccelerometerData to our SensorReading - Graceful failure when sensor unavailable - Pragma directives for conditional warnings **DesktopAccelerometerService (11 tests):** - Timer-based simulated sensor readings (~60Hz) - Generates realistic noise around 1g gravity baseline - Device-at-rest simulation: X≈0, Y≈0, Z≈1 - Includes SimulateShake() method for manual testing - Always reports IsSupported=true (simulation available) **ShakeDetectionService Integration:** - Updated constructor to require IAccelerometerService (breaking change) - StartMonitoring: subscribes to ReadingChanged, calls accelerometer.Start() - StopMonitoring: unsubscribes and calls accelerometer.Stop() - OnAccelerometerReadingChanged: pipes readings to ProcessAccelerometerReading - All existing shake detection logic unchanged **Platform-Specific DI:** - MauiProgram.cs uses conditional compilation - iOS/macOS Catalyst: IosAccelerometerService - Desktop/other platforms: DesktopAccelerometerService - All services registered as Singleton **Test Updates:** - ShakeDetectionServiceTests: now uses NSubstitute mock accelerometer - ServiceIntegrationTests: uses DesktopAccelerometerService - AccelerometerServiceContractTests: base class for implementation tests - IosAccelerometerServiceTests: 10 tests with platform-aware assertions - DesktopAccelerometerServiceTests: 11 tests with async timing **Technical Details:** - SensorReading record type with required init properties - Value equality for SensorReading (record type benefit) - Timer disposal in DesktopAccelerometerService - Event subscription safety (check for null, unsubscribe on stop) - 189 tests passing (165 previous + 24 accelerometer) - 0 warnings, 0 errors across all platforms Build verified: net8.0, iOS (net8.0-ios), macOS Catalyst (net8.0-maccatalyst) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
377 lines
11 KiB
C#
377 lines
11 KiB
C#
using FluentAssertions;
|
|
using HihaArvio.Models;
|
|
using HihaArvio.Services;
|
|
using HihaArvio.Services.Interfaces;
|
|
using NSubstitute;
|
|
|
|
namespace HihaArvio.Tests.Services;
|
|
|
|
public class ShakeDetectionServiceTests
|
|
{
|
|
private readonly IAccelerometerService _mockAccelerometer;
|
|
private readonly IShakeDetectionService _service;
|
|
|
|
public ShakeDetectionServiceTests()
|
|
{
|
|
_mockAccelerometer = Substitute.For<IAccelerometerService>();
|
|
_service = new ShakeDetectionService(_mockAccelerometer);
|
|
}
|
|
|
|
#region Initialization and State Tests
|
|
|
|
[Fact]
|
|
public void Constructor_ShouldInitializeWithDefaultState()
|
|
{
|
|
// Assert
|
|
_service.IsMonitoring.Should().BeFalse();
|
|
_service.CurrentShakeData.Should().NotBeNull();
|
|
_service.CurrentShakeData.IsShaking.Should().BeFalse();
|
|
_service.CurrentShakeData.Intensity.Should().Be(0.0);
|
|
_service.CurrentShakeData.Duration.Should().Be(TimeSpan.Zero);
|
|
}
|
|
|
|
[Fact]
|
|
public void StartMonitoring_ShouldSetIsMonitoringToTrue()
|
|
{
|
|
// Act
|
|
_service.StartMonitoring();
|
|
|
|
// Assert
|
|
_service.IsMonitoring.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void StopMonitoring_ShouldSetIsMonitoringToFalse()
|
|
{
|
|
// Arrange
|
|
_service.StartMonitoring();
|
|
|
|
// Act
|
|
_service.StopMonitoring();
|
|
|
|
// Assert
|
|
_service.IsMonitoring.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void StartMonitoring_MultipleTimes_ShouldRemainsTrue()
|
|
{
|
|
// Act
|
|
_service.StartMonitoring();
|
|
_service.StartMonitoring();
|
|
_service.StartMonitoring();
|
|
|
|
// Assert
|
|
_service.IsMonitoring.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Reset_ShouldClearShakeState()
|
|
{
|
|
// Arrange - Simulate shake
|
|
_service.StartMonitoring();
|
|
_service.ProcessAccelerometerReading(3.0, 3.0, 3.0); // Strong shake
|
|
_service.CurrentShakeData.IsShaking.Should().BeTrue();
|
|
|
|
// Act
|
|
_service.Reset();
|
|
|
|
// Assert
|
|
_service.CurrentShakeData.IsShaking.Should().BeFalse();
|
|
_service.CurrentShakeData.Intensity.Should().Be(0.0);
|
|
_service.CurrentShakeData.Duration.Should().Be(TimeSpan.Zero);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Accelerometer Processing Tests
|
|
|
|
[Fact]
|
|
public void ProcessAccelerometerReading_WhenNotMonitoring_ShouldNotUpdateState()
|
|
{
|
|
// Arrange
|
|
_service.StopMonitoring();
|
|
|
|
// Act
|
|
_service.ProcessAccelerometerReading(2.0, 2.0, 2.0);
|
|
|
|
// Assert
|
|
_service.CurrentShakeData.IsShaking.Should().BeFalse();
|
|
_service.CurrentShakeData.Intensity.Should().Be(0.0);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessAccelerometerReading_WithLowAcceleration_ShouldNotDetectShake()
|
|
{
|
|
// Arrange
|
|
_service.StartMonitoring();
|
|
|
|
// Act - Simulate device at rest (1g gravity)
|
|
_service.ProcessAccelerometerReading(0.0, 0.0, 1.0);
|
|
|
|
// Assert
|
|
_service.CurrentShakeData.IsShaking.Should().BeFalse();
|
|
_service.CurrentShakeData.Intensity.Should().Be(0.0);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessAccelerometerReading_WithModerateShake_ShouldDetectShake()
|
|
{
|
|
// Arrange
|
|
_service.StartMonitoring();
|
|
|
|
// Act - Simulate moderate shake (3g total acceleration, 2g shake after subtracting 1g gravity)
|
|
_service.ProcessAccelerometerReading(2.0, 1.5, 1.5);
|
|
|
|
// Assert
|
|
_service.CurrentShakeData.IsShaking.Should().BeTrue();
|
|
_service.CurrentShakeData.Intensity.Should().BeGreaterThan(0.0);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessAccelerometerReading_WithStrongShake_ShouldDetectHighIntensity()
|
|
{
|
|
// Arrange
|
|
_service.StartMonitoring();
|
|
|
|
// Act - Simulate strong shake (4g total acceleration)
|
|
_service.ProcessAccelerometerReading(3.0, 2.0, 1.5);
|
|
|
|
// Assert
|
|
_service.CurrentShakeData.IsShaking.Should().BeTrue();
|
|
_service.CurrentShakeData.Intensity.Should().BeGreaterThan(0.5);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessAccelerometerReading_ShouldNormalizeIntensityBetweenZeroAndOne()
|
|
{
|
|
// Arrange
|
|
_service.StartMonitoring();
|
|
|
|
// Act - Test various shake strengths
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
var x = Random.Shared.NextDouble() * 6.0 - 3.0;
|
|
var y = Random.Shared.NextDouble() * 6.0 - 3.0;
|
|
var z = Random.Shared.NextDouble() * 6.0 - 3.0;
|
|
_service.ProcessAccelerometerReading(x, y, z);
|
|
|
|
// Assert
|
|
_service.CurrentShakeData.Intensity.Should().BeInRange(0.0, 1.0);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Shake Duration Tests
|
|
|
|
[Fact]
|
|
public void ProcessAccelerometerReading_DuringShake_ShouldIncreaseDuration()
|
|
{
|
|
// Arrange
|
|
_service.StartMonitoring();
|
|
|
|
// Act - Simulate continuous shake
|
|
_service.ProcessAccelerometerReading(2.5, 2.0, 1.5);
|
|
var initialDuration = _service.CurrentShakeData.Duration;
|
|
|
|
Thread.Sleep(100); // Wait 100ms
|
|
_service.ProcessAccelerometerReading(2.5, 2.0, 1.5);
|
|
var laterDuration = _service.CurrentShakeData.Duration;
|
|
|
|
// Assert
|
|
laterDuration.Should().BeGreaterThan(initialDuration);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessAccelerometerReading_AfterShakeStops_ShouldResetDuration()
|
|
{
|
|
// Arrange
|
|
_service.StartMonitoring();
|
|
|
|
// Act - Start shake
|
|
_service.ProcessAccelerometerReading(2.5, 2.0, 1.5);
|
|
_service.CurrentShakeData.IsShaking.Should().BeTrue();
|
|
Thread.Sleep(50);
|
|
_service.ProcessAccelerometerReading(2.5, 2.0, 1.5);
|
|
var shakeDuration = _service.CurrentShakeData.Duration;
|
|
shakeDuration.Should().BeGreaterThan(TimeSpan.Zero);
|
|
|
|
// Stop shake
|
|
Thread.Sleep(50);
|
|
_service.ProcessAccelerometerReading(0.0, 0.0, 1.0);
|
|
_service.CurrentShakeData.IsShaking.Should().BeFalse();
|
|
|
|
// Start new shake
|
|
Thread.Sleep(50);
|
|
_service.ProcessAccelerometerReading(2.5, 2.0, 1.5);
|
|
|
|
// Assert - Duration should reset for new shake
|
|
_service.CurrentShakeData.Duration.Should().BeLessThan(shakeDuration);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Event Tests
|
|
|
|
[Fact]
|
|
public void ShakeDataChanged_ShouldFireWhenShakeStarts()
|
|
{
|
|
// Arrange
|
|
_service.StartMonitoring();
|
|
ShakeData? capturedData = null;
|
|
_service.ShakeDataChanged += (sender, data) => capturedData = data;
|
|
|
|
// Act
|
|
_service.ProcessAccelerometerReading(2.5, 2.0, 1.5);
|
|
|
|
// Assert
|
|
capturedData.Should().NotBeNull();
|
|
capturedData!.IsShaking.Should().BeTrue();
|
|
capturedData.Intensity.Should().BeGreaterThan(0.0);
|
|
}
|
|
|
|
[Fact]
|
|
public void ShakeDataChanged_ShouldFireWhenShakeStops()
|
|
{
|
|
// Arrange
|
|
_service.StartMonitoring();
|
|
_service.ProcessAccelerometerReading(2.5, 2.0, 1.5); // Start shake
|
|
|
|
var eventFiredCount = 0;
|
|
ShakeData? lastCapturedData = null;
|
|
_service.ShakeDataChanged += (sender, data) =>
|
|
{
|
|
eventFiredCount++;
|
|
lastCapturedData = data;
|
|
};
|
|
|
|
// Act - Stop shake
|
|
_service.ProcessAccelerometerReading(0.0, 0.0, 1.0);
|
|
|
|
// Assert
|
|
eventFiredCount.Should().BeGreaterThan(0);
|
|
lastCapturedData.Should().NotBeNull();
|
|
lastCapturedData!.IsShaking.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void ShakeDataChanged_ShouldFireWhenIntensityChanges()
|
|
{
|
|
// Arrange
|
|
_service.StartMonitoring();
|
|
|
|
var intensities = new List<double>();
|
|
_service.ShakeDataChanged += (sender, data) => intensities.Add(data.Intensity);
|
|
|
|
// Act - Start with gentle shake, then increase intensity
|
|
_service.ProcessAccelerometerReading(2.0, 1.5, 1.0); // Gentle shake (~2.5g total, ~1.5g shake)
|
|
_service.ProcessAccelerometerReading(3.0, 2.5, 2.0); // Strong shake (~4.4g total, ~3.4g shake)
|
|
|
|
// Assert
|
|
intensities.Should().HaveCountGreaterThanOrEqualTo(2);
|
|
intensities.Last().Should().BeGreaterThan(intensities.First());
|
|
}
|
|
|
|
[Fact]
|
|
public void ShakeDataChanged_WhenNotMonitoring_ShouldNotFire()
|
|
{
|
|
// Arrange
|
|
_service.StopMonitoring();
|
|
var eventFired = false;
|
|
_service.ShakeDataChanged += (sender, data) => eventFired = true;
|
|
|
|
// Act
|
|
_service.ProcessAccelerometerReading(2.5, 2.0, 1.5);
|
|
|
|
// Assert
|
|
eventFired.Should().BeFalse();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Intensity Calculation Tests
|
|
|
|
[Fact]
|
|
public void ProcessAccelerometerReading_ShouldCalculateIntensityFromMagnitude()
|
|
{
|
|
// Arrange
|
|
_service.StartMonitoring();
|
|
|
|
// Act & Assert - Test known values
|
|
// Magnitude = sqrt(3^2 + 4^2 + 0^2) = 5.0
|
|
_service.ProcessAccelerometerReading(3.0, 4.0, 0.0);
|
|
_service.CurrentShakeData.IsShaking.Should().BeTrue();
|
|
_service.CurrentShakeData.Intensity.Should().BeGreaterThan(0.5);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessAccelerometerReading_WithZeroAcceleration_ShouldHaveZeroIntensity()
|
|
{
|
|
// Arrange
|
|
_service.StartMonitoring();
|
|
|
|
// Act
|
|
_service.ProcessAccelerometerReading(0.0, 0.0, 0.0);
|
|
|
|
// Assert
|
|
_service.CurrentShakeData.Intensity.Should().Be(0.0);
|
|
_service.CurrentShakeData.IsShaking.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessAccelerometerReading_WithNegativeValues_ShouldCalculateCorrectMagnitude()
|
|
{
|
|
// Arrange
|
|
_service.StartMonitoring();
|
|
|
|
// Act - Negative values should be squared, making them positive
|
|
_service.ProcessAccelerometerReading(-2.0, -2.0, -2.0);
|
|
|
|
// Assert
|
|
_service.CurrentShakeData.IsShaking.Should().BeTrue();
|
|
_service.CurrentShakeData.Intensity.Should().BeGreaterThan(0.0);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Edge Cases
|
|
|
|
[Fact]
|
|
public void Reset_WhenNotShaking_ShouldNotThrow()
|
|
{
|
|
// Act & Assert
|
|
var act = () => _service.Reset();
|
|
act.Should().NotThrow();
|
|
}
|
|
|
|
[Fact]
|
|
public void StopMonitoring_WhenNotStarted_ShouldNotThrow()
|
|
{
|
|
// Act & Assert
|
|
var act = () => _service.StopMonitoring();
|
|
act.Should().NotThrow();
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessAccelerometerReading_WithExtremeValues_ShouldNotThrow()
|
|
{
|
|
// Arrange
|
|
_service.StartMonitoring();
|
|
|
|
// Act & Assert
|
|
var act = () =>
|
|
{
|
|
_service.ProcessAccelerometerReading(100.0, 100.0, 100.0);
|
|
_service.ProcessAccelerometerReading(-100.0, -100.0, -100.0);
|
|
_service.ProcessAccelerometerReading(double.MaxValue / 1000, 0, 0);
|
|
};
|
|
act.Should().NotThrow();
|
|
|
|
// Intensity should still be normalized
|
|
_service.CurrentShakeData.Intensity.Should().BeInRange(0.0, 1.0);
|
|
}
|
|
|
|
#endregion
|
|
}
|