Files
XplorePlane/XplorePlane.Tests/Services/ViewportRecordingPropertyTests.cs
T
zhengxuan.zhang 318d1813b8 录屏功能
2026-05-07 15:12:06 +08:00

181 lines
8.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (0059) and SS is zero-padded seconds (0059),
/// 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;
});
}
}
}