位置节点增加保存图像到本地的功能;支持输入图像
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using System.Linq;
|
||||
using XplorePlane.Helpers;
|
||||
|
||||
namespace XplorePlane.Tests.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// FsCheck property-based tests for FileNameSanitizer.
|
||||
/// Property 1: 文件名非法字符清理
|
||||
/// Validates: Requirements 1.2
|
||||
/// </summary>
|
||||
public class FileNameSanitizerPropertyTests
|
||||
{
|
||||
private static readonly char[] InvalidChars =
|
||||
new[] { '\\', '/', ':', '*', '?', '"', '<', '>', '|' };
|
||||
|
||||
/// <summary>
|
||||
/// 生成包含各种字符(含非法字符)的文件名字符串
|
||||
/// </summary>
|
||||
private static Arbitrary<string> FileNameArb()
|
||||
{
|
||||
// Mix of normal chars, unicode chars, and illegal file system chars
|
||||
var charGen = Gen.Frequency(
|
||||
(5, Gen.Choose(0x20, 0x7E).Select(i => (char)i)), // printable ASCII
|
||||
(2, Gen.Elements(InvalidChars)), // illegal chars
|
||||
(2, Gen.Choose(0x4E00, 0x9FFF).Select(i => (char)i)), // CJK characters
|
||||
(1, Gen.Elements('_', '-', '.', ' ')) // common filename chars
|
||||
);
|
||||
|
||||
var gen = from len in Gen.Choose(1, 50)
|
||||
from chars in Gen.ListOf<char>(charGen, len)
|
||||
select new string(chars.ToArray());
|
||||
|
||||
return gen.ToArbitrary();
|
||||
}
|
||||
|
||||
// ── Property 1: Output never contains any illegal characters ─────────
|
||||
|
||||
/// <summary>
|
||||
/// **Validates: Requirements 1.2**
|
||||
/// For any input string, the sanitized output must not contain any of the 9 illegal characters.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property SanitizedOutput_NeverContainsIllegalCharacters()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
FileNameArb(),
|
||||
input =>
|
||||
{
|
||||
var result = FileNameSanitizer.Sanitize(input);
|
||||
return !result.Any(c => InvalidChars.Contains(c));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Property 1 (cont): Legal characters are preserved unchanged ──────
|
||||
|
||||
/// <summary>
|
||||
/// **Validates: Requirements 1.2**
|
||||
/// For any input string, characters that are NOT illegal must appear unchanged
|
||||
/// at the same position in the output.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property SanitizedOutput_PreservesLegalCharacters()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
FileNameArb(),
|
||||
input =>
|
||||
{
|
||||
var result = FileNameSanitizer.Sanitize(input);
|
||||
|
||||
for (int i = 0; i < input.Length; i++)
|
||||
{
|
||||
if (!InvalidChars.Contains(input[i]))
|
||||
{
|
||||
if (result[i] != input[i])
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Property 1 (cont): Output length equals input length ─────────────
|
||||
|
||||
/// <summary>
|
||||
/// **Validates: Requirements 1.2**
|
||||
/// For any non-null, non-empty input, the output length equals the input length.
|
||||
/// For null or empty input, the output is "_".
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property SanitizedOutput_LengthEqualsInputLength()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
FileNameArb(),
|
||||
input =>
|
||||
{
|
||||
var result = FileNameSanitizer.Sanitize(input);
|
||||
return result.Length == input.Length;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Property 1 (cont): Sanitize is idempotent ────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// **Validates: Requirements 1.2**
|
||||
/// Sanitizing the output a second time produces the same result (idempotent).
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property SanitizedOutput_IsIdempotent()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
FileNameArb(),
|
||||
input =>
|
||||
{
|
||||
var once = FileNameSanitizer.Sanitize(input);
|
||||
var twice = FileNameSanitizer.Sanitize(once);
|
||||
return once == twice;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Edge case: null and empty input ──────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// **Validates: Requirements 1.2**
|
||||
/// Null or empty input always returns "_".
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property NullOrEmptyInput_ReturnsUnderscore()
|
||||
{
|
||||
var gen = Gen.Elements<string>(null, "", null, "");
|
||||
|
||||
return Prop.ForAll(
|
||||
gen.ToArbitrary(),
|
||||
input =>
|
||||
{
|
||||
var result = FileNameSanitizer.Sanitize(input);
|
||||
return result == "_";
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using XplorePlane.Helpers;
|
||||
|
||||
namespace XplorePlane.Tests.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// FsCheck property-based tests for ManualImageValidator.
|
||||
/// Property 4: ManualImagePath 验证正确性
|
||||
/// **Validates: Requirements 2.1, 2.3**
|
||||
/// </summary>
|
||||
public class ManualImageValidatorPropertyTests
|
||||
{
|
||||
// ── Generators ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Generates null, empty, or whitespace-only strings.
|
||||
/// </summary>
|
||||
private static Gen<string> NullOrWhitespaceGen()
|
||||
{
|
||||
return Gen.OneOf(
|
||||
Gen.Constant((string)null),
|
||||
Gen.Constant(string.Empty),
|
||||
Gen.Constant(" "),
|
||||
Gen.Constant(" "),
|
||||
Gen.Constant("\t"),
|
||||
Gen.Constant("\t \n"),
|
||||
Gen.Constant(" \r\n "));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates paths longer than 260 characters (non-whitespace).
|
||||
/// </summary>
|
||||
private static Gen<string> PathTooLongGen()
|
||||
{
|
||||
// Generate lengths from 261 to 500
|
||||
return Gen.Choose(261, 500).Select(len =>
|
||||
{
|
||||
// Use a valid-looking path prefix to ensure it's not whitespace
|
||||
var prefix = @"C:\Images\";
|
||||
var remaining = len - prefix.Length;
|
||||
if (remaining <= 0) remaining = 1;
|
||||
return prefix + new string('a', remaining);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates non-empty paths with length ≤ 260 that point to non-existent files.
|
||||
/// Uses random alphanumeric characters to ensure the file won't exist.
|
||||
/// </summary>
|
||||
private static Gen<string> NonExistentPathGen()
|
||||
{
|
||||
// Generate a path that is very unlikely to exist on disk
|
||||
var charGen = Gen.Elements(
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
|
||||
'A', 'B', 'C', 'D', 'E', 'F', '0', '1', '2', '3');
|
||||
|
||||
return from len in Gen.Choose(5, 50)
|
||||
from chars in Gen.ArrayOf(charGen).Resize(len)
|
||||
select @"C:\NonExistent_" + new string(chars) + ".bmp";
|
||||
}
|
||||
|
||||
// ── Property Tests ──────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Property 4.1: Null/whitespace/empty paths always return Empty.
|
||||
/// For any null, empty, or whitespace-only string, Validate SHALL return Empty.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property NullOrWhitespacePaths_ReturnEmpty()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
NullOrWhitespaceGen().ToArbitrary(),
|
||||
path =>
|
||||
{
|
||||
var result = ManualImageValidator.Validate(path);
|
||||
return result == ManualImageValidationResult.Empty;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Property 4.2: Paths longer than 260 characters always return PathTooLong.
|
||||
/// For any non-whitespace path with length > 260, Validate SHALL return PathTooLong.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property PathsLongerThan260_ReturnPathTooLong()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
PathTooLongGen().ToArbitrary(),
|
||||
path =>
|
||||
{
|
||||
var result = ManualImageValidator.Validate(path);
|
||||
return result == ManualImageValidationResult.PathTooLong;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Property 4.3: Non-existent file paths (non-empty, ≤260) return FileNotFound.
|
||||
/// For any non-whitespace path with length ≤ 260 that does not exist on disk,
|
||||
/// Validate SHALL return FileNotFound.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property NonExistentPaths_ReturnFileNotFound()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
NonExistentPathGen().ToArbitrary(),
|
||||
path =>
|
||||
{
|
||||
var result = ManualImageValidator.Validate(path);
|
||||
return result == ManualImageValidationResult.FileNotFound;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Property 4.4: The result is always one of the defined enum values (exhaustive classification).
|
||||
/// For any arbitrary string, Validate SHALL return a value within the defined enum range.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property Result_IsAlwaysDefinedEnumValue()
|
||||
{
|
||||
// Generate arbitrary strings including nulls, empty, whitespace, and random paths
|
||||
var charGen = Gen.Frequency(
|
||||
(5, Gen.Choose(0x20, 0x7E).Select(i => (char)i)), // printable ASCII
|
||||
(2, Gen.Choose(0x4E00, 0x9FFF).Select(i => (char)i)), // CJK characters
|
||||
(1, Gen.Elements('\\', '/', ':', '*', '?', '"', '<', '>', '|')),
|
||||
(1, Gen.Elements(' ', '\t', '\n', '\r'))
|
||||
);
|
||||
|
||||
var randomStringGen = from len in Gen.Choose(0, 300)
|
||||
from chars in Gen.ArrayOf(charGen).Resize(len)
|
||||
select new string(chars);
|
||||
|
||||
var anyStringGen = Gen.OneOf(
|
||||
Gen.Constant((string)null),
|
||||
Gen.Constant(string.Empty),
|
||||
Gen.Constant(" "),
|
||||
randomStringGen);
|
||||
|
||||
return Prop.ForAll(
|
||||
anyStringGen.ToArbitrary(),
|
||||
path =>
|
||||
{
|
||||
var result = ManualImageValidator.Validate(path);
|
||||
return Enum.IsDefined(typeof(ManualImageValidationResult), result);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Property 4.5: Existing files with supported extensions return Valid.
|
||||
/// Creates temporary files with supported extensions and verifies Validate returns Valid.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 20)]
|
||||
public Property ExistingFilesWithSupportedExtension_ReturnValid()
|
||||
{
|
||||
var supportedExtGen = Gen.Elements(".bmp", ".png", ".tiff", ".tif");
|
||||
|
||||
return Prop.ForAll(
|
||||
supportedExtGen.ToArbitrary(),
|
||||
ext =>
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "ManualImageValidatorTests");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var tempFile = Path.Combine(tempDir, $"test_{Guid.NewGuid():N}{ext}");
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllBytes(tempFile, new byte[] { 0x00 });
|
||||
var result = ManualImageValidator.Validate(tempFile);
|
||||
return result == ManualImageValidationResult.Valid;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempFile))
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Property 4.6: Existing files with unsupported extensions return UnsupportedFormat.
|
||||
/// Creates temporary files with unsupported extensions and verifies Validate returns UnsupportedFormat.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 20)]
|
||||
public Property ExistingFilesWithUnsupportedExtension_ReturnUnsupportedFormat()
|
||||
{
|
||||
var unsupportedExtGen = Gen.Elements(".jpg", ".jpeg", ".gif", ".svg", ".webp", ".txt", ".dat");
|
||||
|
||||
return Prop.ForAll(
|
||||
unsupportedExtGen.ToArbitrary(),
|
||||
ext =>
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "ManualImageValidatorTests");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var tempFile = Path.Combine(tempDir, $"test_{Guid.NewGuid():N}{ext}");
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllBytes(tempFile, new byte[] { 0x00 });
|
||||
var result = ManualImageValidator.Validate(tempFile);
|
||||
return result == ManualImageValidationResult.UnsupportedFormat;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempFile))
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user