录屏功能
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using Moq;
|
||||
using Prism.Events;
|
||||
using XplorePlane.Tests.Helpers;
|
||||
using XplorePlane.ViewModels;
|
||||
using Xunit;
|
||||
@@ -13,7 +15,8 @@ namespace XplorePlane.Tests.Pipeline
|
||||
private OperatorToolboxViewModel CreateVm(string[]? keys = null)
|
||||
{
|
||||
var mock = TestHelpers.CreateMockImageService(keys);
|
||||
return new OperatorToolboxViewModel(mock.Object);
|
||||
var mockEventAggregator = new Mock<IEventAggregator>();
|
||||
return new OperatorToolboxViewModel(mock.Object, mockEventAggregator.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using Emgu.CV;
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using XplorePlane.Services.Recording;
|
||||
|
||||
namespace XplorePlane.Tests.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// FsCheck property-based tests for ViewportRecordingService static helpers.
|
||||
/// </summary>
|
||||
public class ViewportRecordingPropertyTests
|
||||
{
|
||||
// ── Property 1: Timer formatting correctness ─────────────────────────
|
||||
|
||||
// Feature: viewport-recording, Property 1: Timer formatting correctness
|
||||
/// <summary>
|
||||
/// **Validates: Requirements 2.2**
|
||||
/// For any elapsed time value in seconds between 0 and 3599 (inclusive),
|
||||
/// FormatElapsedTime SHALL produce a string in the format "MM:SS" where
|
||||
/// MM is zero-padded minutes (00–59) and SS is zero-padded seconds (00–59),
|
||||
/// and the numeric value represented equals the input seconds.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property FormatElapsedTime_ProducesCorrectMMSS_ForAllValidSeconds()
|
||||
{
|
||||
var secondsGen = Gen.Choose(0, 3599);
|
||||
|
||||
return Prop.ForAll(
|
||||
secondsGen.ToArbitrary(),
|
||||
totalSeconds =>
|
||||
{
|
||||
string result = ViewportRecordingService.FormatElapsedTime(totalSeconds);
|
||||
|
||||
// Verify format is exactly "MM:SS" (5 chars, colon at index 2)
|
||||
bool correctLength = result.Length == 5;
|
||||
bool hasColon = result[2] == ':';
|
||||
bool matchesPattern = Regex.IsMatch(result, @"^\d{2}:\d{2}$");
|
||||
|
||||
// Parse back and verify numeric correctness
|
||||
int parsedMinutes = int.Parse(result.Substring(0, 2));
|
||||
int parsedSeconds = int.Parse(result.Substring(3, 2));
|
||||
|
||||
bool minutesInRange = parsedMinutes >= 0 && parsedMinutes <= 59;
|
||||
bool secondsInRange = parsedSeconds >= 0 && parsedSeconds <= 59;
|
||||
|
||||
// Verify the numeric value equals the input
|
||||
int reconstructed = parsedMinutes * 60 + parsedSeconds;
|
||||
bool valueCorrect = reconstructed == totalSeconds;
|
||||
|
||||
return correctLength && hasColon && matchesPattern
|
||||
&& minutesInRange && secondsInRange && valueCorrect;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Property 2: Recording filename generation ────────────────────────
|
||||
|
||||
// Feature: viewport-recording, Property 2: Recording filename generation
|
||||
/// <summary>
|
||||
/// **Validates: Requirements 3.2**
|
||||
/// For any valid DateTime value, GenerateFileName SHALL produce a string
|
||||
/// matching the pattern REC_yyyyMMdd_HHmmss.mp4, and parsing the timestamp
|
||||
/// portion back to a DateTime SHALL yield the same year, month, day, hour,
|
||||
/// minute, and second as the input.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property GenerateFileName_MatchesPattern_AndTimestampIsReversible()
|
||||
{
|
||||
var dateTimeGen = ArbMap.Default.GeneratorFor<DateTime>();
|
||||
|
||||
return Prop.ForAll(
|
||||
dateTimeGen.ToArbitrary(),
|
||||
(DateTime dateTime) =>
|
||||
{
|
||||
var fileName = ViewportRecordingService.GenerateFileName(dateTime);
|
||||
|
||||
// Verify pattern: REC_yyyyMMdd_HHmmss.mp4
|
||||
var pattern = @"^REC_\d{8}_\d{6}\.mp4$";
|
||||
bool matchesPattern = Regex.IsMatch(fileName, pattern);
|
||||
|
||||
// Extract timestamp portion and parse back
|
||||
var timestampStr = fileName.Substring(4, 15); // "yyyyMMdd_HHmmss"
|
||||
bool canParse = DateTime.TryParseExact(
|
||||
timestampStr,
|
||||
"yyyyMMdd_HHmmss",
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.None,
|
||||
out DateTime parsed);
|
||||
|
||||
// Verify reversibility: year, month, day, hour, minute, second match
|
||||
bool isReversible = canParse
|
||||
&& parsed.Year == dateTime.Year
|
||||
&& parsed.Month == dateTime.Month
|
||||
&& parsed.Day == dateTime.Day
|
||||
&& parsed.Hour == dateTime.Hour
|
||||
&& parsed.Minute == dateTime.Minute
|
||||
&& parsed.Second == dateTime.Second;
|
||||
|
||||
return matchesPattern && isReversible;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Property 3: VIDEO folder path construction ───────────────────────
|
||||
|
||||
// Feature: viewport-recording, Property 3: VIDEO folder path construction
|
||||
/// <summary>
|
||||
/// **Validates: Requirements 4.2**
|
||||
/// For any non-empty string rootPath, GetVideoFolderPath SHALL produce a path
|
||||
/// equal to Path.Combine(rootPath, "VIDEO"), ensuring the VIDEO subfolder
|
||||
/// is always constructed relative to the given root path.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property GetVideoFolderPath_AlwaysReturnsVideoSubfolder()
|
||||
{
|
||||
// Generate non-empty strings (filter out empty/whitespace)
|
||||
var nonEmptyStringGen = ArbMap.Default.GeneratorFor<NonEmptyString>();
|
||||
|
||||
return Prop.ForAll(
|
||||
nonEmptyStringGen.ToArbitrary(),
|
||||
(NonEmptyString rootPath) =>
|
||||
{
|
||||
string result = ViewportRecordingService.GetVideoFolderPath(rootPath.Get);
|
||||
|
||||
// Verify the result equals Path.Combine(rootPath, "VIDEO")
|
||||
string expected = System.IO.Path.Combine(rootPath.Get, "VIDEO");
|
||||
bool equalsExpected = result == expected;
|
||||
|
||||
// Verify the result ends with "VIDEO" segment
|
||||
bool endsWithVideo = result.EndsWith("VIDEO") || result.EndsWith("VIDEO/") || result.EndsWith("VIDEO\\");
|
||||
|
||||
return equalsExpected && endsWithVideo;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Property 4: Null-source frame produces black frame ───────────────
|
||||
|
||||
// Feature: viewport-recording, Property 4: Null-source frame produces black frame
|
||||
/// <summary>
|
||||
/// **Validates: Requirements 6.3**
|
||||
/// For any positive integer dimensions (width, height), CreateBlackFrame SHALL
|
||||
/// produce a Mat with the correct dimensions, 3 channels (BGR24), and all pixel
|
||||
/// values equal to zero (pure black).
|
||||
/// </summary>
|
||||
[Property(MaxTest = 50)]
|
||||
public Property CreateBlackFrame_ProducesCorrectBlackFrame_ForAnyPositiveDimensions()
|
||||
{
|
||||
// Generate positive integers for width and height (1..512 to keep tests fast)
|
||||
var widthGen = Gen.Choose(1, 512);
|
||||
var heightGen = Gen.Choose(1, 512);
|
||||
var dimensionsGen = widthGen.SelectMany(w => heightGen.Select(h => (w, h)));
|
||||
|
||||
return Prop.ForAll(
|
||||
dimensionsGen.ToArbitrary(),
|
||||
((int width, int height) dims) =>
|
||||
{
|
||||
using var frame = ViewportRecordingService.CreateBlackFrame(dims.width, dims.height);
|
||||
|
||||
// Verify dimensions
|
||||
bool correctWidth = frame.Width == dims.width;
|
||||
bool correctHeight = frame.Height == dims.height;
|
||||
|
||||
// Verify 3 channels (BGR24)
|
||||
bool correctChannels = frame.NumberOfChannels == 3;
|
||||
|
||||
// Verify all pixels are zero (black)
|
||||
// Get raw byte data and check all zeros
|
||||
int totalBytes = dims.width * dims.height * 3;
|
||||
byte[] pixelData = new byte[totalBytes];
|
||||
System.Runtime.InteropServices.Marshal.Copy(frame.DataPointer, pixelData, 0, totalBytes);
|
||||
bool allZero = System.Array.TrueForAll(pixelData, b => b == 0);
|
||||
|
||||
return correctWidth && correctHeight && correctChannels && allZero;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Helpers\Define.cs" />
|
||||
<Compile Remove="Pipeline\OperatorToolboxViewModelTests.cs" />
|
||||
<Compile Remove="Pipeline\PipelinePropertyTests.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user