mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-08 20:27:36 +02:00
[PowerRename] Support using photo metadata to replace in the PowerRename (#41728)
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request 1. Introduce WIC for power rename and add new class WICMetadataExtractor to use WIC to extract metadata. 2. Add some patterns for metadata extract. 3. Support XMP and EXIF metadata extract. 4. Add test data for xmp and exif extractor 5. Add attribution for the test data uploader. UI: <img width="2052" height="1415" alt="image" src="https://github.com/user-attachments/assets/9051b12e-4e66-4fdc-a4d4-3bada661c235" /> <img width="284" height="170" alt="image" src="https://github.com/user-attachments/assets/2fd67193-77a7-48f0-a5ac-08a69fe64e55" /> <img width="715" height="1160" alt="image" src="https://github.com/user-attachments/assets/5fa68a8c-d129-44dd-b747-099dfbcded12" /> demo: https://github.com/user-attachments/assets/e90bc206-62e5-4101-ada2-3187ee7e2039 <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #5612 - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [x] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed --------- Co-authored-by: Yu Leng <yuleng@microsoft.com>
This commit is contained in:
766
src/modules/powerrename/unittests/HelpersTests.cpp
Normal file
766
src/modules/powerrename/unittests/HelpersTests.cpp
Normal file
@@ -0,0 +1,766 @@
|
||||
#include "pch.h"
|
||||
#include "Helpers.h"
|
||||
#include "MetadataPatternExtractor.h"
|
||||
#include "MetadataTypes.h"
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace HelpersTests
|
||||
{
|
||||
TEST_CLASS(GetMetadataFileNameTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(BasicPatternReplacement)
|
||||
{
|
||||
// Test basic pattern replacement with available metadata
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
patterns[L"ISO"] = L"ISO 400";
|
||||
patterns[L"DATE_TAKEN_YYYY"] = L"2024";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$CAMERA_MAKE_$ISO", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_Canon_ISO 400", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(PatternWithoutValueShowsPatternName)
|
||||
{
|
||||
// Test that patterns without values show the pattern name with $ prefix
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
// ISO is not in the map
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$CAMERA_MAKE_$ISO", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_Canon_$ISO", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(EmptyPatternShowsPatternName)
|
||||
{
|
||||
// Test that patterns with empty value show the pattern name with $ prefix
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
patterns[L"ISO"] = L"";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$CAMERA_MAKE_$ISO", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_Canon_$ISO", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(EscapedDollarSigns)
|
||||
{
|
||||
// Test that $$ is converted to single $
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"ISO"] = L"ISO 400";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$$_$ISO", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_$_ISO 400", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(MultipleEscapedDollarSigns)
|
||||
{
|
||||
// Test that $$$$ is converted to $$
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"ISO"] = L"ISO 400";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$$$$price", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_$$price", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(OddDollarSignsWithPattern)
|
||||
{
|
||||
// Test that $$$ becomes $ followed by pattern
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"ISO"] = L"ISO 400";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$$$ISO", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_$ISO 400", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(LongestPatternMatchPriority)
|
||||
{
|
||||
// Test that longer patterns are matched first (DATE_TAKEN_YYYY vs DATE_TAKEN_YY)
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"DATE_TAKEN_YYYY"] = L"2024";
|
||||
patterns[L"DATE_TAKEN_YY"] = L"24";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$DATE_TAKEN_YYYY", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_2024", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(MultiplePatterns)
|
||||
{
|
||||
// Test multiple patterns in one string
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
patterns[L"CAMERA_MODEL"] = L"EOS R5";
|
||||
patterns[L"ISO"] = L"ISO 800";
|
||||
patterns[L"DATE_TAKEN_YYYY"] = L"2024";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH,
|
||||
L"$DATE_TAKEN_YYYY-$CAMERA_MAKE-$CAMERA_MODEL-$ISO", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"2024-Canon-EOS R5-ISO 800", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(UnrecognizedPatternIgnored)
|
||||
{
|
||||
// Test that unrecognized patterns are not replaced
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$CAMERA_MAKE_$INVALID_PATTERN", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_Canon_$INVALID_PATTERN", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(NoPatterns)
|
||||
{
|
||||
// Test string with no patterns
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_name_without_patterns", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_name_without_patterns", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(EmptyInput)
|
||||
{
|
||||
// Test with empty input string
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"", patterns);
|
||||
|
||||
Assert::IsTrue(FAILED(hr));
|
||||
}
|
||||
|
||||
TEST_METHOD(NullInput)
|
||||
{
|
||||
// Test with null input
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, nullptr, patterns);
|
||||
|
||||
Assert::IsTrue(FAILED(hr));
|
||||
}
|
||||
|
||||
TEST_METHOD(DollarAtEnd)
|
||||
{
|
||||
// Test dollar sign at the end of string
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"ISO"] = L"ISO 400";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$ISO$", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_ISO 400$", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(ThreeDollarsAtEnd)
|
||||
{
|
||||
// Test three dollar signs at the end
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo$$$", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo$$$", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComplexMixedScenario)
|
||||
{
|
||||
// Test complex scenario with mixed patterns, escapes, and regular text
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
patterns[L"ISO"] = L"ISO 400";
|
||||
patterns[L"APERTURE"] = L"f/2.8";
|
||||
patterns[L"LENS"] = L""; // Empty value
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH,
|
||||
L"$$price_$CAMERA_MAKE_$$$ISO_$APERTURE_$LENS_$$end", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"$price_Canon_$ISO 400_f/2.8_$LENS_$end", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(AllEXIFPatterns)
|
||||
{
|
||||
// Test with various EXIF patterns
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"WIDTH"] = L"4000";
|
||||
patterns[L"HEIGHT"] = L"3000";
|
||||
patterns[L"FOCAL"] = L"50mm";
|
||||
patterns[L"SHUTTER"] = L"1/100s";
|
||||
patterns[L"FLASH"] = L"Flash Off";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH,
|
||||
L"photo_$WIDTH x $HEIGHT_$FOCAL_$SHUTTER_$FLASH", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_4000 x 3000_50mm_1/100s_Flash Off", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(AllXMPPatterns)
|
||||
{
|
||||
// Test with various XMP patterns
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"TITLE"] = L"Sunset";
|
||||
patterns[L"CREATOR"] = L"John Doe";
|
||||
patterns[L"DESCRIPTION"] = L"Beautiful sunset";
|
||||
patterns[L"CREATE_DATE_YYYY"] = L"2024";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH,
|
||||
L"$CREATE_DATE_YYYY-$TITLE-by-$CREATOR", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"2024-Sunset-by-John Doe", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DateComponentPatterns)
|
||||
{
|
||||
// Test date component patterns
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"DATE_TAKEN_YYYY"] = L"2024";
|
||||
patterns[L"DATE_TAKEN_MM"] = L"03";
|
||||
patterns[L"DATE_TAKEN_DD"] = L"15";
|
||||
patterns[L"DATE_TAKEN_HH"] = L"14";
|
||||
patterns[L"DATE_TAKEN_mm"] = L"30";
|
||||
patterns[L"DATE_TAKEN_SS"] = L"45";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH,
|
||||
L"photo_$DATE_TAKEN_YYYY-$DATE_TAKEN_MM-$DATE_TAKEN_DD_$DATE_TAKEN_HH-$DATE_TAKEN_mm-$DATE_TAKEN_SS",
|
||||
patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_2024-03-15_14-30-45", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(SpecialCharactersInValues)
|
||||
{
|
||||
// Test that special characters in metadata values are preserved
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"TITLE"] = L"Photo (with) [brackets] & symbols!";
|
||||
patterns[L"DESCRIPTION"] = L"Test: value; with, punctuation.";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH,
|
||||
L"$TITLE - $DESCRIPTION", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"Photo (with) [brackets] & symbols! - Test: value; with, punctuation.", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(ConsecutivePatternsWithoutSeparator)
|
||||
{
|
||||
// Test consecutive patterns without separator
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
patterns[L"CAMERA_MODEL"] = L"R5";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$CAMERA_MAKE$CAMERA_MODEL", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"CanonR5", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(PatternAtStart)
|
||||
{
|
||||
// Test pattern at the beginning of string
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$CAMERA_MAKE_photo", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"Canon_photo", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(PatternAtEnd)
|
||||
{
|
||||
// Test pattern at the end of string
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$CAMERA_MAKE", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_Canon", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(OnlyPattern)
|
||||
{
|
||||
// Test string with only a pattern
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$CAMERA_MAKE", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"Canon", result);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(PatternMatchingTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(VerifyLongestPatternMatching)
|
||||
{
|
||||
// This test verifies the greedy matching behavior
|
||||
// When we have overlapping pattern names, the longest should be matched first
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"DATE_TAKEN_Y"] = L"4";
|
||||
patterns[L"DATE_TAKEN_YY"] = L"24";
|
||||
patterns[L"DATE_TAKEN_YYYY"] = L"2024";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
|
||||
// Should match YYYY (longest)
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$DATE_TAKEN_YYYY", patterns);
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"2024", result);
|
||||
|
||||
// Should match YY (available pattern)
|
||||
hr = GetMetadataFileName(result, MAX_PATH, L"$DATE_TAKEN_YY", patterns);
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"24", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(PartialPatternNames)
|
||||
{
|
||||
// Test that partial pattern names don't match longer patterns
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MODEL"] = L"EOS R5";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
// CAMERA is not a valid pattern, should not match
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$CAMERA_MODEL", patterns);
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"EOS R5", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CaseSensitivePatterns)
|
||||
{
|
||||
// Test that pattern names are case-sensitive
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
// lowercase should not match
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$camera_make", patterns);
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"$camera_make", result); // Not replaced
|
||||
}
|
||||
|
||||
TEST_METHOD(EmptyPatternMap)
|
||||
{
|
||||
// Test with empty pattern map
|
||||
PowerRenameLib::MetadataPatternMap patterns; // Empty
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$ISO_$CAMERA_MAKE", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
// Patterns should show with $ prefix since they're valid but have no values
|
||||
Assert::AreEqual(L"photo_$ISO_$CAMERA_MAKE", result);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(EdgeCaseTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(VeryLongString)
|
||||
{
|
||||
// Test with a very long input string
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
|
||||
std::wstring longInput = L"prefix_";
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
longInput += L"$CAMERA_MAKE_";
|
||||
}
|
||||
|
||||
wchar_t result[4096] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, 4096, longInput.c_str(), patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
// Verify it starts correctly
|
||||
Assert::IsTrue(wcsstr(result, L"prefix_Canon_") == result);
|
||||
}
|
||||
|
||||
TEST_METHOD(ManyConsecutiveDollars)
|
||||
{
|
||||
// Test with many consecutive dollar signs
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
// 8 dollars should become 4 dollars
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo$$$$$$$$name", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo$$$$name", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(OnlyDollars)
|
||||
{
|
||||
// Test string with only dollar signs
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$$$$", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"$$", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(UnicodeCharacters)
|
||||
{
|
||||
// Test with unicode characters in pattern values
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"TITLE"] = L"照片_фото_φωτογραφία";
|
||||
patterns[L"CREATOR"] = L"张三_Иван_Γιάννης";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$TITLE-$CREATOR", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"照片_фото_φωτογραφία-张三_Иван_Γιάννης", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(SingleDollar)
|
||||
{
|
||||
// Test with single dollar not followed by pattern
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"price$100", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"price$100", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DollarFollowedByNumber)
|
||||
{
|
||||
// Test dollar followed by numbers (not a pattern)
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"cost_$123.45", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"cost_$123.45", result);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(GetDatedFileNameTests)
|
||||
{
|
||||
public:
|
||||
// Helper to get a fixed test time for consistent testing
|
||||
SYSTEMTIME GetTestTime()
|
||||
{
|
||||
SYSTEMTIME testTime = { 0 };
|
||||
testTime.wYear = 2024;
|
||||
testTime.wMonth = 3; // March
|
||||
testTime.wDay = 15; // 15th
|
||||
testTime.wHour = 14; // 2 PM (24-hour format)
|
||||
testTime.wMinute = 30;
|
||||
testTime.wSecond = 45;
|
||||
testTime.wMilliseconds = 123;
|
||||
testTime.wDayOfWeek = 5; // Friday (0=Sunday, 5=Friday)
|
||||
return testTime;
|
||||
}
|
||||
|
||||
// Category 1: Tests for invalid patterns with extra characters (verify negative lookahead prevents wrong matching)
|
||||
|
||||
TEST_METHOD(InvalidPattern_YYY_NotMatched)
|
||||
{
|
||||
// Test $YYY (3 Y's) is not a valid pattern and should remain unchanged
|
||||
// Negative lookahead in $YY(?!Y) prevents matching $YYY
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$YYY", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_$YYY", result); // $YYY is invalid, should remain unchanged
|
||||
}
|
||||
|
||||
TEST_METHOD(InvalidPattern_DDD_NotPartiallyMatched)
|
||||
{
|
||||
// Test that $DDD (short weekday) is not confused with $DD (2-digit day)
|
||||
// This verifies negative lookahead works correctly
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$DDD", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_Fri", result); // Should be "Fri", not "15D"
|
||||
}
|
||||
|
||||
TEST_METHOD(InvalidPattern_MMM_NotPartiallyMatched)
|
||||
{
|
||||
// Test that $MMM (short month name) is not confused with $MM (2-digit month)
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$MMM", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_Mar", result); // Should be "Mar", not "03M"
|
||||
}
|
||||
|
||||
TEST_METHOD(InvalidPattern_HHH_NotMatched)
|
||||
{
|
||||
// Test $HHH (3 H's) is not valid and negative lookahead prevents $HH from matching
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$HHH", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_$HHH", result); // Should remain unchanged
|
||||
}
|
||||
|
||||
TEST_METHOD(SeparatedPatterns_SingleY)
|
||||
{
|
||||
// Test multiple $Y with separators works correctly
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$Y-$Y-$Y", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_4-4-4", result); // Each $Y outputs "4" (from 2024)
|
||||
}
|
||||
|
||||
TEST_METHOD(SeparatedPatterns_SingleD)
|
||||
{
|
||||
// Test multiple $D with separators works correctly
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$D.$D.$D", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_15.15.15", result); // Each $D outputs "15"
|
||||
}
|
||||
|
||||
// Category 2: Tests for mixed length patterns (verify longer patterns don't get matched incorrectly)
|
||||
|
||||
TEST_METHOD(MixedLengthYear_QuadFollowedBySingle)
|
||||
{
|
||||
// Test $YYYY$Y - should be 2024 + 4
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$YYYY$Y", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_20244", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(MixedLengthDay_TripleFollowedBySingle)
|
||||
{
|
||||
// Test $DDD$D - should be "Fri" + "15"
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$DDD$D", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_Fri15", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(MixedLengthDay_QuadFollowedByDouble)
|
||||
{
|
||||
// Test $DDDD$DD - should be "Friday" + "15"
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$DDDD$DD", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_Friday15", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(MixedLengthMonth_TripleFollowedBySingle)
|
||||
{
|
||||
// Test $MMM$M - should be "Mar" + "3"
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$MMM$M", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_Mar3", result);
|
||||
}
|
||||
|
||||
// Category 3: Tests for boundary conditions (patterns at start, end, with special chars)
|
||||
|
||||
TEST_METHOD(PatternAtStart)
|
||||
{
|
||||
// Test pattern at the very start of filename
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"$YYYY$M$D", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"2024315", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(PatternAtEnd)
|
||||
{
|
||||
// Test pattern at the very end of filename
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$Y", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_4", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(PatternWithSpecialChars)
|
||||
{
|
||||
// Test patterns surrounded by special characters
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file-$Y.$Y-$M", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file-4.4-3", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(EmptyFileName)
|
||||
{
|
||||
// Test with empty input string - should return E_INVALIDARG
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"", testTime);
|
||||
|
||||
Assert::IsTrue(FAILED(hr)); // Empty string should fail
|
||||
Assert::AreEqual(E_INVALIDARG, hr);
|
||||
}
|
||||
|
||||
// Category 4: Tests to explicitly verify negative lookahead is working
|
||||
|
||||
TEST_METHOD(NegativeLookahead_YearNotMatchedInYYYY)
|
||||
{
|
||||
// Verify $Y doesn't match when part of $YYYY
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$YYYY", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_2024", result); // Should be "2024", not "202Y"
|
||||
}
|
||||
|
||||
TEST_METHOD(NegativeLookahead_MonthNotMatchedInMMM)
|
||||
{
|
||||
// Verify $M doesn't match when part of $MMM
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$MMM", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_Mar", result); // Should be "Mar", not "3ar"
|
||||
}
|
||||
|
||||
TEST_METHOD(NegativeLookahead_DayNotMatchedInDDDD)
|
||||
{
|
||||
// Verify $D doesn't match when part of $DDDD
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$DDDD", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_Friday", result); // Should be "Friday", not "15riday"
|
||||
}
|
||||
|
||||
TEST_METHOD(NegativeLookahead_HourNotMatchedInHH)
|
||||
{
|
||||
// Verify $H doesn't match when part of $HH
|
||||
// Note: $HH is 12-hour format, so 14:00 (2 PM) displays as "02"
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$HH", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_02", result); // 14:00 in 12-hour format is "02 PM"
|
||||
}
|
||||
|
||||
TEST_METHOD(NegativeLookahead_MillisecondNotMatchedInFFF)
|
||||
{
|
||||
// Verify $f doesn't match when part of $fff
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$fff", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_123", result); // Should be "123", not "1ff"
|
||||
}
|
||||
|
||||
// Category 5: Complex mixed scenarios
|
||||
|
||||
TEST_METHOD(ComplexMixedPattern_AllFormats)
|
||||
{
|
||||
// Test a complex realistic filename with mixed pattern lengths
|
||||
// Note: Using $hh for 24-hour format instead of $HH (which is 12-hour)
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"Photo_$YYYY-$MM-$DD_$hh-$mm-$ss_$fff", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"Photo_2024-03-15_14-30-45_123", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComplexMixedPattern_WithSeparators)
|
||||
{
|
||||
// Test multiple patterns of different lengths with separators
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"$YYYY_$Y-$Y_$MM_$M", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"2024_4-4_03_3", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComplexMixedPattern_DayFormats)
|
||||
{
|
||||
// Test all day format variations in one string
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"$D-$DD-$DDD-$DDDD", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"15-15-Fri-Friday", result);
|
||||
}
|
||||
};
|
||||
}
|
||||
487
src/modules/powerrename/unittests/MetadataFormatHelperTests.cpp
Normal file
487
src/modules/powerrename/unittests/MetadataFormatHelperTests.cpp
Normal file
@@ -0,0 +1,487 @@
|
||||
#include "pch.h"
|
||||
#include "MetadataFormatHelper.h"
|
||||
#include <cmath>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
using namespace PowerRenameLib;
|
||||
|
||||
namespace MetadataFormatHelperTests
|
||||
{
|
||||
TEST_CLASS(FormatApertureTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(FormatAperture_ValidValue)
|
||||
{
|
||||
// Test formatting a typical aperture value
|
||||
std::wstring result = MetadataFormatHelper::FormatAperture(2.8);
|
||||
Assert::AreEqual(L"f/2.8", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatAperture_SmallValue)
|
||||
{
|
||||
// Test small aperture (large f-number)
|
||||
std::wstring result = MetadataFormatHelper::FormatAperture(1.4);
|
||||
Assert::AreEqual(L"f/1.4", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatAperture_LargeValue)
|
||||
{
|
||||
// Test large aperture (small f-number)
|
||||
std::wstring result = MetadataFormatHelper::FormatAperture(22.0);
|
||||
Assert::AreEqual(L"f/22.0", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatAperture_RoundedValue)
|
||||
{
|
||||
// Test rounding to one decimal place
|
||||
std::wstring result = MetadataFormatHelper::FormatAperture(5.66666);
|
||||
Assert::AreEqual(L"f/5.7", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatAperture_Zero)
|
||||
{
|
||||
// Test zero value
|
||||
std::wstring result = MetadataFormatHelper::FormatAperture(0.0);
|
||||
Assert::AreEqual(L"f/0.0", result.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(FormatShutterSpeedTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(FormatShutterSpeed_FastSpeed)
|
||||
{
|
||||
// Test fast shutter speed (fraction of second)
|
||||
std::wstring result = MetadataFormatHelper::FormatShutterSpeed(0.002);
|
||||
Assert::AreEqual(L"1/500s", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatShutterSpeed_VeryFastSpeed)
|
||||
{
|
||||
// Test very fast shutter speed
|
||||
std::wstring result = MetadataFormatHelper::FormatShutterSpeed(0.0001);
|
||||
Assert::AreEqual(L"1/10000s", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatShutterSpeed_SlowSpeed)
|
||||
{
|
||||
// Test slow shutter speed (more than 1 second)
|
||||
std::wstring result = MetadataFormatHelper::FormatShutterSpeed(2.5);
|
||||
Assert::AreEqual(L"2.5s", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatShutterSpeed_OneSecond)
|
||||
{
|
||||
// Test exactly 1 second
|
||||
std::wstring result = MetadataFormatHelper::FormatShutterSpeed(1.0);
|
||||
Assert::AreEqual(L"1.0s", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatShutterSpeed_VerySlowSpeed)
|
||||
{
|
||||
// Test very slow shutter speed (< 1 second but close)
|
||||
std::wstring result = MetadataFormatHelper::FormatShutterSpeed(0.5);
|
||||
Assert::AreEqual(L"1/2s", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatShutterSpeed_Zero)
|
||||
{
|
||||
// Test zero value
|
||||
std::wstring result = MetadataFormatHelper::FormatShutterSpeed(0.0);
|
||||
Assert::AreEqual(L"0", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatShutterSpeed_Negative)
|
||||
{
|
||||
// Test negative value (invalid but should handle gracefully)
|
||||
std::wstring result = MetadataFormatHelper::FormatShutterSpeed(-1.0);
|
||||
Assert::AreEqual(L"0", result.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(FormatISOTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(FormatISO_TypicalValue)
|
||||
{
|
||||
// Test typical ISO value
|
||||
std::wstring result = MetadataFormatHelper::FormatISO(400);
|
||||
Assert::AreEqual(L"ISO 400", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatISO_LowValue)
|
||||
{
|
||||
// Test low ISO value
|
||||
std::wstring result = MetadataFormatHelper::FormatISO(100);
|
||||
Assert::AreEqual(L"ISO 100", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatISO_HighValue)
|
||||
{
|
||||
// Test high ISO value
|
||||
std::wstring result = MetadataFormatHelper::FormatISO(12800);
|
||||
Assert::AreEqual(L"ISO 12800", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatISO_Zero)
|
||||
{
|
||||
// Test zero value
|
||||
std::wstring result = MetadataFormatHelper::FormatISO(0);
|
||||
Assert::AreEqual(L"ISO", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatISO_Negative)
|
||||
{
|
||||
// Test negative value (invalid but should handle gracefully)
|
||||
std::wstring result = MetadataFormatHelper::FormatISO(-100);
|
||||
Assert::AreEqual(L"ISO", result.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(FormatFlashTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(FormatFlash_Off)
|
||||
{
|
||||
// Test flash off (bit 0 = 0)
|
||||
std::wstring result = MetadataFormatHelper::FormatFlash(0x0);
|
||||
Assert::AreEqual(L"Flash Off", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatFlash_On)
|
||||
{
|
||||
// Test flash on (bit 0 = 1)
|
||||
std::wstring result = MetadataFormatHelper::FormatFlash(0x1);
|
||||
Assert::AreEqual(L"Flash On", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatFlash_OnWithAdditionalFlags)
|
||||
{
|
||||
// Test flash on with additional flags
|
||||
std::wstring result = MetadataFormatHelper::FormatFlash(0x5); // 0b0101 = fired, return detected
|
||||
Assert::AreEqual(L"Flash On", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatFlash_OffWithAdditionalFlags)
|
||||
{
|
||||
// Test flash off with additional flags
|
||||
std::wstring result = MetadataFormatHelper::FormatFlash(0x10); // Bit 0 is 0
|
||||
Assert::AreEqual(L"Flash Off", result.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(FormatCoordinateTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(FormatCoordinate_NorthLatitude)
|
||||
{
|
||||
// Test north latitude
|
||||
std::wstring result = MetadataFormatHelper::FormatCoordinate(40.7128, true);
|
||||
Assert::AreEqual(L"40°42.77'N", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatCoordinate_SouthLatitude)
|
||||
{
|
||||
// Test south latitude
|
||||
std::wstring result = MetadataFormatHelper::FormatCoordinate(-33.8688, true);
|
||||
Assert::AreEqual(L"33°52.13'S", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatCoordinate_EastLongitude)
|
||||
{
|
||||
// Test east longitude
|
||||
std::wstring result = MetadataFormatHelper::FormatCoordinate(151.2093, false);
|
||||
Assert::AreEqual(L"151°12.56'E", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatCoordinate_WestLongitude)
|
||||
{
|
||||
// Test west longitude
|
||||
std::wstring result = MetadataFormatHelper::FormatCoordinate(-74.0060, false);
|
||||
Assert::AreEqual(L"74°0.36'W", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatCoordinate_ZeroLatitude)
|
||||
{
|
||||
// Test equator (0 degrees latitude)
|
||||
std::wstring result = MetadataFormatHelper::FormatCoordinate(0.0, true);
|
||||
Assert::AreEqual(L"0°0.00'N", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatCoordinate_ZeroLongitude)
|
||||
{
|
||||
// Test prime meridian (0 degrees longitude)
|
||||
std::wstring result = MetadataFormatHelper::FormatCoordinate(0.0, false);
|
||||
Assert::AreEqual(L"0°0.00'E", result.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(FormatSystemTimeTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(FormatSystemTime_ValidDateTime)
|
||||
{
|
||||
// Test formatting a valid date and time
|
||||
SYSTEMTIME st = { 0 };
|
||||
st.wYear = 2024;
|
||||
st.wMonth = 3;
|
||||
st.wDay = 15;
|
||||
st.wHour = 14;
|
||||
st.wMinute = 30;
|
||||
st.wSecond = 45;
|
||||
|
||||
std::wstring result = MetadataFormatHelper::FormatSystemTime(st);
|
||||
Assert::AreEqual(L"2024-03-15 14:30:45", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatSystemTime_Midnight)
|
||||
{
|
||||
// Test midnight time
|
||||
SYSTEMTIME st = { 0 };
|
||||
st.wYear = 2024;
|
||||
st.wMonth = 1;
|
||||
st.wDay = 1;
|
||||
st.wHour = 0;
|
||||
st.wMinute = 0;
|
||||
st.wSecond = 0;
|
||||
|
||||
std::wstring result = MetadataFormatHelper::FormatSystemTime(st);
|
||||
Assert::AreEqual(L"2024-01-01 00:00:00", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatSystemTime_EndOfDay)
|
||||
{
|
||||
// Test end of day time
|
||||
SYSTEMTIME st = { 0 };
|
||||
st.wYear = 2024;
|
||||
st.wMonth = 12;
|
||||
st.wDay = 31;
|
||||
st.wHour = 23;
|
||||
st.wMinute = 59;
|
||||
st.wSecond = 59;
|
||||
|
||||
std::wstring result = MetadataFormatHelper::FormatSystemTime(st);
|
||||
Assert::AreEqual(L"2024-12-31 23:59:59", result.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(ParseSingleRationalTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(ParseSingleRational_ValidValue)
|
||||
{
|
||||
// Test parsing a valid rational: 5/2 = 2.5
|
||||
uint8_t bytes[] = { 5, 0, 0, 0, 2, 0, 0, 0 };
|
||||
double result = MetadataFormatHelper::ParseSingleRational(bytes, 0);
|
||||
Assert::AreEqual(2.5, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleRational_IntegerResult)
|
||||
{
|
||||
// Test parsing rational that results in integer: 10/5 = 2.0
|
||||
uint8_t bytes[] = { 10, 0, 0, 0, 5, 0, 0, 0 };
|
||||
double result = MetadataFormatHelper::ParseSingleRational(bytes, 0);
|
||||
Assert::AreEqual(2.0, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleRational_LargeNumerator)
|
||||
{
|
||||
// Test parsing with large numerator: 1000/100 = 10.0
|
||||
uint8_t bytes[] = { 0xE8, 0x03, 0, 0, 100, 0, 0, 0 }; // 1000 in little-endian
|
||||
double result = MetadataFormatHelper::ParseSingleRational(bytes, 0);
|
||||
Assert::AreEqual(10.0, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleRational_ZeroDenominator)
|
||||
{
|
||||
// Test parsing with zero denominator (should return 0.0)
|
||||
uint8_t bytes[] = { 5, 0, 0, 0, 0, 0, 0, 0 };
|
||||
double result = MetadataFormatHelper::ParseSingleRational(bytes, 0);
|
||||
Assert::AreEqual(0.0, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleRational_ZeroNumerator)
|
||||
{
|
||||
// Test parsing with zero numerator: 0/5 = 0.0
|
||||
uint8_t bytes[] = { 0, 0, 0, 0, 5, 0, 0, 0 };
|
||||
double result = MetadataFormatHelper::ParseSingleRational(bytes, 0);
|
||||
Assert::AreEqual(0.0, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleRational_WithOffset)
|
||||
{
|
||||
// Test parsing with offset
|
||||
uint8_t bytes[] = { 0xFF, 0xFF, 0xFF, 0xFF, 10, 0, 0, 0, 5, 0, 0, 0 }; // Offset = 4
|
||||
double result = MetadataFormatHelper::ParseSingleRational(bytes, 4);
|
||||
Assert::AreEqual(2.0, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleRational_NullPointer)
|
||||
{
|
||||
// Test with null pointer (should return 0.0)
|
||||
double result = MetadataFormatHelper::ParseSingleRational(nullptr, 0);
|
||||
Assert::AreEqual(0.0, result, 0.001);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(ParseSingleSRationalTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(ParseSingleSRational_PositiveValue)
|
||||
{
|
||||
// Test parsing positive signed rational: 5/2 = 2.5
|
||||
uint8_t bytes[] = { 5, 0, 0, 0, 2, 0, 0, 0 };
|
||||
double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0);
|
||||
Assert::AreEqual(2.5, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleSRational_NegativeNumerator)
|
||||
{
|
||||
// Test parsing negative numerator: -5/2 = -2.5
|
||||
uint8_t bytes[] = { 0xFB, 0xFF, 0xFF, 0xFF, 2, 0, 0, 0 }; // -5 in two's complement
|
||||
double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0);
|
||||
Assert::AreEqual(-2.5, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleSRational_NegativeDenominator)
|
||||
{
|
||||
// Test parsing negative denominator: 5/-2 = -2.5
|
||||
uint8_t bytes[] = { 5, 0, 0, 0, 0xFE, 0xFF, 0xFF, 0xFF }; // -2 in two's complement
|
||||
double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0);
|
||||
Assert::AreEqual(-2.5, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleSRational_BothNegative)
|
||||
{
|
||||
// Test parsing both negative: -5/-2 = 2.5
|
||||
uint8_t bytes[] = { 0xFB, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF };
|
||||
double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0);
|
||||
Assert::AreEqual(2.5, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleSRational_ExposureBias)
|
||||
{
|
||||
// Test typical exposure bias value: -1/3 ≈ -0.333
|
||||
uint8_t bytes[] = { 0xFF, 0xFF, 0xFF, 0xFF, 3, 0, 0, 0 }; // -1/3
|
||||
double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0);
|
||||
Assert::AreEqual(-0.333, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleSRational_ZeroDenominator)
|
||||
{
|
||||
// Test with zero denominator (should return 0.0)
|
||||
uint8_t bytes[] = { 5, 0, 0, 0, 0, 0, 0, 0 };
|
||||
double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0);
|
||||
Assert::AreEqual(0.0, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleSRational_NullPointer)
|
||||
{
|
||||
// Test with null pointer (should return 0.0)
|
||||
double result = MetadataFormatHelper::ParseSingleSRational(nullptr, 0);
|
||||
Assert::AreEqual(0.0, result, 0.001);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(SanitizeForFileNameTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(SanitizeForFileName_ValidString)
|
||||
{
|
||||
// Test string without illegal characters
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"Canon EOS 5D");
|
||||
Assert::AreEqual(L"Canon EOS 5D", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_WithColon)
|
||||
{
|
||||
// Test string with colon (illegal character)
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"Photo:001");
|
||||
Assert::AreEqual(L"Photo_001", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_WithSlashes)
|
||||
{
|
||||
// Test string with forward and backward slashes
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"Photos/2024\\January");
|
||||
Assert::AreEqual(L"Photos_2024_January", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_WithMultipleIllegalChars)
|
||||
{
|
||||
// Test string with multiple illegal characters
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"<Test>:File|Name*?.txt");
|
||||
Assert::AreEqual(L"_Test__File_Name__.txt", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_WithQuotes)
|
||||
{
|
||||
// Test string with quotes
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"Photo \"Best Shot\"");
|
||||
Assert::AreEqual(L"Photo _Best Shot_", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_WithTrailingDot)
|
||||
{
|
||||
// Test string with trailing dot (should be removed)
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"filename.");
|
||||
Assert::AreEqual(L"filename", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_WithTrailingSpace)
|
||||
{
|
||||
// Test string with trailing space (should be removed)
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"filename ");
|
||||
Assert::AreEqual(L"filename", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_WithMultipleTrailingDotsAndSpaces)
|
||||
{
|
||||
// Test string with multiple trailing dots and spaces
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"filename. . ");
|
||||
Assert::AreEqual(L"filename", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_WithControlCharacters)
|
||||
{
|
||||
// Test string with control characters
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"File\x01Name\x1F");
|
||||
Assert::AreEqual(L"File_Name_", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_EmptyString)
|
||||
{
|
||||
// Test empty string
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"");
|
||||
Assert::AreEqual(L"", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_OnlyIllegalCharacters)
|
||||
{
|
||||
// Test string with only illegal characters
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"<>:\"/\\|?*");
|
||||
Assert::AreEqual(L"_________", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_OnlyTrailingCharacters)
|
||||
{
|
||||
// Test string with only dots and spaces (should return empty)
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L". . ");
|
||||
Assert::AreEqual(L"", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_UnicodeCharacters)
|
||||
{
|
||||
// Test string with valid Unicode characters
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"照片_2024年");
|
||||
Assert::AreEqual(L"照片_2024年", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_MixedContent)
|
||||
{
|
||||
// Test realistic metadata string with multiple issues
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"Copyright © 2024: John/Jane Doe. ");
|
||||
Assert::AreEqual(L"Copyright © 2024_ John_Jane Doe", result.c_str());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -62,6 +62,12 @@ IFACEMETHODIMP CMockPowerRenameRegExEvents::OnFileTimeChanged(_In_ SYSTEMTIME fi
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
IFACEMETHODIMP CMockPowerRenameRegExEvents::OnMetadataChanged()
|
||||
{
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
|
||||
HRESULT CMockPowerRenameRegExEvents::s_CreateInstance(_Outptr_ IPowerRenameRegExEvents** ppsrree)
|
||||
{
|
||||
*ppsrree = nullptr;
|
||||
@@ -74,3 +80,4 @@ HRESULT CMockPowerRenameRegExEvents::s_CreateInstance(_Outptr_ IPowerRenameRegEx
|
||||
}
|
||||
return hr;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ public:
|
||||
IFACEMETHODIMP OnReplaceTermChanged(_In_ PCWSTR replaceTerm);
|
||||
IFACEMETHODIMP OnFlagsChanged(_In_ DWORD flags);
|
||||
IFACEMETHODIMP OnFileTimeChanged(_In_ SYSTEMTIME fileTime);
|
||||
IFACEMETHODIMP OnMetadataChanged();
|
||||
|
||||
static HRESULT s_CreateInstance(_Outptr_ IPowerRenameRegExEvents** ppsrree);
|
||||
|
||||
@@ -39,3 +40,4 @@ public:
|
||||
SYSTEMTIME m_fileTime = { 0 };
|
||||
long m_refCount;
|
||||
};
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalLibraryDirectories>$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalDependencies>$(OutDir)\..\..\WinUI3Apps\PowerRenameLib.lib;comctl32.lib;pathcch.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;Pathcch.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>$(OutDir)\..\..\WinUI3Apps\PowerRenameLib.lib;comctl32.lib;pathcch.lib;windowscodecs.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;Pathcch.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
@@ -49,11 +49,14 @@
|
||||
<ClInclude Include="CommonRegExTests.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="HelpersTests.cpp" />
|
||||
<ClCompile Include="MockPowerRenameItem.cpp" />
|
||||
<ClCompile Include="MockPowerRenameManagerEvents.cpp" />
|
||||
<ClCompile Include="MockPowerRenameRegExEvents.cpp" />
|
||||
<ClCompile Include="PowerRenameRegExBoostTests.cpp" />
|
||||
<ClCompile Include="PowerRenameManagerTests.cpp" />
|
||||
<ClCompile Include="MetadataFormatHelperTests.cpp" />
|
||||
<ClCompile Include="WICMetadataExtractorTests.cpp" />
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
@@ -73,8 +76,30 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
<!-- Include all test data files for deployment -->
|
||||
<None Include="testdata\exif_test.jpg">
|
||||
<DeploymentContent>true</DeploymentContent>
|
||||
</None>
|
||||
<None Include="testdata\exif_test_2.jpg">
|
||||
<DeploymentContent>true</DeploymentContent>
|
||||
</None>
|
||||
<None Include="testdata\xmp_test.jpg">
|
||||
<DeploymentContent>true</DeploymentContent>
|
||||
</None>
|
||||
<None Include="testdata\xmp_test_2.jpg">
|
||||
<DeploymentContent>true</DeploymentContent>
|
||||
</None>
|
||||
<None Include="testdata\ATTRIBUTION.md">
|
||||
<DeploymentContent>true</DeploymentContent>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<Target Name="CopyTestData" AfterTargets="Build">
|
||||
<ItemGroup>
|
||||
<TestDataFiles Include="$(ProjectDir)testdata\**\*.*" />
|
||||
</ItemGroup>
|
||||
<Copy SourceFiles="@(TestDataFiles)" DestinationFolder="$(OutDir)testdata\%(RecursiveDir)" SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
|
||||
<Import Project="..\..\..\..\packages\boost.1.87.0\build\boost.targets" Condition="Exists('..\..\..\..\packages\boost.1.87.0\build\boost.targets')" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<ClCompile Include="HelpersTests.cpp" />
|
||||
<ClCompile Include="MockPowerRenameItem.cpp" />
|
||||
<ClCompile Include="MockPowerRenameManagerEvents.cpp" />
|
||||
<ClCompile Include="MockPowerRenameRegExEvents.cpp" />
|
||||
@@ -30,6 +31,9 @@
|
||||
<Filter Include="Header Files">
|
||||
<UniqueIdentifier>{d34a343a-52ef-4296-83c9-a94fa62062ff}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="testdata">
|
||||
<UniqueIdentifier>{8c9f3e2d-1a4b-4e5f-9c7d-2b8a6f3e1d4c}</UniqueIdentifier>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="PowerRenameUnitTests.rc">
|
||||
@@ -38,5 +42,20 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
<None Include="testdata\exif_test.jpg">
|
||||
<Filter>testdata</Filter>
|
||||
</None>
|
||||
<None Include="testdata\exif_test_2.jpg">
|
||||
<Filter>testdata</Filter>
|
||||
</None>
|
||||
<None Include="testdata\xmp_test.jpg">
|
||||
<Filter>testdata</Filter>
|
||||
</None>
|
||||
<None Include="testdata\xmp_test_2.jpg">
|
||||
<Filter>testdata</Filter>
|
||||
</None>
|
||||
<None Include="testdata\ATTRIBUTION.md">
|
||||
<Filter>testdata</Filter>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
244
src/modules/powerrename/unittests/WICMetadataExtractorTests.cpp
Normal file
244
src/modules/powerrename/unittests/WICMetadataExtractorTests.cpp
Normal file
@@ -0,0 +1,244 @@
|
||||
#include "pch.h"
|
||||
#include "WICMetadataExtractor.h"
|
||||
#include <filesystem>
|
||||
#include <sstream>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
using namespace PowerRenameLib;
|
||||
|
||||
namespace WICMetadataExtractorTests
|
||||
{
|
||||
// Helper function to get the test data directory path
|
||||
std::wstring GetTestDataPath()
|
||||
{
|
||||
// Get the directory where the test DLL is located
|
||||
// When running with vstest, we need to get the DLL module handle
|
||||
HMODULE hModule = nullptr;
|
||||
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(&GetTestDataPath),
|
||||
&hModule);
|
||||
|
||||
wchar_t modulePath[MAX_PATH];
|
||||
GetModuleFileNameW(hModule, modulePath, MAX_PATH);
|
||||
std::filesystem::path dllPath(modulePath);
|
||||
|
||||
// Navigate to the test data directory
|
||||
// The test data is in the output directory alongside the DLL
|
||||
std::filesystem::path testDataPath = dllPath.parent_path() / L"testdata";
|
||||
|
||||
return testDataPath.wstring();
|
||||
}
|
||||
|
||||
TEST_CLASS(ExtractEXIFMetadataTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(ExtractEXIF_InvalidFile_ReturnsFalse)
|
||||
{
|
||||
// Test that EXIF extraction fails for nonexistent file
|
||||
WICMetadataExtractor extractor;
|
||||
EXIFMetadata metadata;
|
||||
|
||||
std::wstring testFile = GetTestDataPath() + L"\\nonexistent.jpg";
|
||||
bool result = extractor.ExtractEXIFMetadata(testFile, metadata);
|
||||
|
||||
Assert::IsFalse(result, L"EXIF extraction should fail for nonexistent file");
|
||||
}
|
||||
|
||||
TEST_METHOD(ExtractEXIF_ExifTest_AllFields)
|
||||
{
|
||||
// Test exif_test.jpg which contains comprehensive EXIF data
|
||||
WICMetadataExtractor extractor;
|
||||
EXIFMetadata metadata;
|
||||
|
||||
std::wstring testFile = GetTestDataPath() + L"\\exif_test.jpg";
|
||||
bool result = extractor.ExtractEXIFMetadata(testFile, metadata);
|
||||
|
||||
Assert::IsTrue(result, L"EXIF extraction should succeed");
|
||||
|
||||
// Verify all the fields that are in exif_test.jpg
|
||||
Assert::IsTrue(metadata.cameraMake.has_value(), L"Camera make should be present");
|
||||
Assert::AreEqual(L"samsung", metadata.cameraMake.value().c_str(), L"Camera make should be samsung");
|
||||
|
||||
Assert::IsTrue(metadata.cameraModel.has_value(), L"Camera model should be present");
|
||||
Assert::AreEqual(L"SM-G930P", metadata.cameraModel.value().c_str(), L"Camera model should be SM-G930P");
|
||||
|
||||
Assert::IsTrue(metadata.lensModel.has_value(), L"Lens model should be present");
|
||||
Assert::AreEqual(L"Samsung Galaxy S7 Rear Camera", metadata.lensModel.value().c_str(), L"Lens model should match");
|
||||
|
||||
Assert::IsTrue(metadata.iso.has_value(), L"ISO should be present");
|
||||
Assert::AreEqual(40, static_cast<int>(metadata.iso.value()), L"ISO should be 40");
|
||||
|
||||
Assert::IsTrue(metadata.aperture.has_value(), L"Aperture should be present");
|
||||
Assert::AreEqual(1.7, metadata.aperture.value(), 0.01, L"Aperture should be f/1.7");
|
||||
|
||||
Assert::IsTrue(metadata.shutterSpeed.has_value(), L"Shutter speed should be present");
|
||||
Assert::AreEqual(0.000625, metadata.shutterSpeed.value(), 0.000001, L"Shutter speed should be 0.000625s");
|
||||
|
||||
Assert::IsTrue(metadata.focalLength.has_value(), L"Focal length should be present");
|
||||
Assert::AreEqual(4.2, metadata.focalLength.value(), 0.1, L"Focal length should be 4.2mm");
|
||||
|
||||
Assert::IsTrue(metadata.flash.has_value(), L"Flash should be present");
|
||||
Assert::AreEqual(0u, static_cast<unsigned int>(metadata.flash.value()), L"Flash should be 0x0");
|
||||
|
||||
Assert::IsTrue(metadata.exposureBias.has_value(), L"Exposure bias should be present");
|
||||
Assert::AreEqual(0.0, metadata.exposureBias.value(), 0.01, L"Exposure bias should be 0 EV");
|
||||
|
||||
Assert::IsTrue(metadata.author.has_value(), L"Author should be present");
|
||||
Assert::AreEqual(L"Carl Seibert (Exif)", metadata.author.value().c_str(), L"Author should match");
|
||||
|
||||
Assert::IsTrue(metadata.copyright.has_value(), L"Copyright should be present");
|
||||
Assert::IsTrue(metadata.copyright.value().find(L"Carl Seibert") != std::wstring::npos, L"Copyright should contain Carl Seibert");
|
||||
}
|
||||
|
||||
TEST_METHOD(ExtractEXIF_ExifTest2_WidthHeight)
|
||||
{
|
||||
// Test exif_test_2.jpg which only contains width and height
|
||||
WICMetadataExtractor extractor;
|
||||
EXIFMetadata metadata;
|
||||
|
||||
std::wstring testFile = GetTestDataPath() + L"\\exif_test_2.jpg";
|
||||
bool result = extractor.ExtractEXIFMetadata(testFile, metadata);
|
||||
|
||||
Assert::IsTrue(result, L"EXIF extraction should succeed");
|
||||
|
||||
// exif_test_2.jpg only has width and height
|
||||
Assert::IsTrue(metadata.width.has_value(), L"Width should be present");
|
||||
Assert::AreEqual(1080u, static_cast<unsigned int>(metadata.width.value()), L"Width should be 1080px");
|
||||
|
||||
Assert::IsTrue(metadata.height.has_value(), L"Height should be present");
|
||||
Assert::AreEqual(810u, static_cast<unsigned int>(metadata.height.value()), L"Height should be 810px");
|
||||
|
||||
// Other fields should not be present
|
||||
Assert::IsFalse(metadata.cameraMake.has_value(), L"Camera make should not be present in exif_test_2.jpg");
|
||||
Assert::IsFalse(metadata.cameraModel.has_value(), L"Camera model should not be present in exif_test_2.jpg");
|
||||
Assert::IsFalse(metadata.iso.has_value(), L"ISO should not be present in exif_test_2.jpg");
|
||||
}
|
||||
|
||||
TEST_METHOD(ExtractEXIF_ClearCache)
|
||||
{
|
||||
// Test cache clearing works
|
||||
WICMetadataExtractor extractor;
|
||||
EXIFMetadata metadata;
|
||||
|
||||
std::wstring testFile = GetTestDataPath() + L"\\exif_test.jpg";
|
||||
|
||||
bool result1 = extractor.ExtractEXIFMetadata(testFile, metadata);
|
||||
Assert::IsTrue(result1);
|
||||
|
||||
extractor.ClearCache();
|
||||
|
||||
EXIFMetadata metadata2;
|
||||
bool result2 = extractor.ExtractEXIFMetadata(testFile, metadata2);
|
||||
Assert::IsTrue(result2);
|
||||
|
||||
// Both calls should succeed
|
||||
Assert::AreEqual(metadata.cameraMake.value().c_str(), metadata2.cameraMake.value().c_str());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(ExtractXMPMetadataTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(ExtractXMP_InvalidFile_ReturnsFalse)
|
||||
{
|
||||
// Test that XMP extraction fails for nonexistent file
|
||||
WICMetadataExtractor extractor;
|
||||
XMPMetadata metadata;
|
||||
|
||||
std::wstring testFile = GetTestDataPath() + L"\\nonexistent.jpg";
|
||||
bool result = extractor.ExtractXMPMetadata(testFile, metadata);
|
||||
|
||||
Assert::IsFalse(result, L"XMP extraction should fail for nonexistent file");
|
||||
}
|
||||
|
||||
TEST_METHOD(ExtractXMP_XmpTest_AllFields)
|
||||
{
|
||||
// Test xmp_test.jpg which contains comprehensive XMP data
|
||||
WICMetadataExtractor extractor;
|
||||
XMPMetadata metadata;
|
||||
|
||||
std::wstring testFile = GetTestDataPath() + L"\\xmp_test.jpg";
|
||||
bool result = extractor.ExtractXMPMetadata(testFile, metadata);
|
||||
|
||||
Assert::IsTrue(result, L"XMP extraction should succeed");
|
||||
|
||||
// Verify all the fields that are in xmp_test.jpg
|
||||
Assert::IsTrue(metadata.title.has_value(), L"Title should be present");
|
||||
Assert::AreEqual(L"object name here", metadata.title.value().c_str(), L"Title should match");
|
||||
|
||||
Assert::IsTrue(metadata.description.has_value(), L"Description should be present");
|
||||
Assert::IsTrue(metadata.description.value().find(L"This is a metadata test file") != std::wstring::npos,
|
||||
L"Description should contain expected text");
|
||||
|
||||
Assert::IsTrue(metadata.rights.has_value(), L"Rights should be present");
|
||||
Assert::AreEqual(L"metadatamatters.blog", metadata.rights.value().c_str(), L"Rights should match");
|
||||
|
||||
Assert::IsTrue(metadata.creatorTool.has_value(), L"Creator tool should be present");
|
||||
Assert::IsTrue(metadata.creatorTool.value().find(L"Adobe Photoshop Lightroom") != std::wstring::npos,
|
||||
L"Creator tool should contain Lightroom");
|
||||
|
||||
Assert::IsTrue(metadata.documentID.has_value(), L"Document ID should be present");
|
||||
Assert::IsTrue(metadata.documentID.value().find(L"xmp.did:") != std::wstring::npos,
|
||||
L"Document ID should start with xmp.did:");
|
||||
|
||||
Assert::IsTrue(metadata.instanceID.has_value(), L"Instance ID should be present");
|
||||
Assert::IsTrue(metadata.instanceID.value().find(L"xmp.iid:") != std::wstring::npos,
|
||||
L"Instance ID should start with xmp.iid:");
|
||||
|
||||
Assert::IsTrue(metadata.subject.has_value(), L"Subject keywords should be present");
|
||||
Assert::IsTrue(metadata.subject.value().size() > 0, L"Should have at least one keyword");
|
||||
}
|
||||
|
||||
TEST_METHOD(ExtractXMP_XmpTest2_BasicFields)
|
||||
{
|
||||
// Test xmp_test_2.jpg which only contains basic XMP fields
|
||||
WICMetadataExtractor extractor;
|
||||
XMPMetadata metadata;
|
||||
|
||||
std::wstring testFile = GetTestDataPath() + L"\\xmp_test_2.jpg";
|
||||
bool result = extractor.ExtractXMPMetadata(testFile, metadata);
|
||||
|
||||
Assert::IsTrue(result, L"XMP extraction should succeed");
|
||||
|
||||
// xmp_test_2.jpg only has CreatorTool, DocumentID, and InstanceID
|
||||
Assert::IsTrue(metadata.creatorTool.has_value(), L"Creator tool should be present");
|
||||
Assert::IsTrue(metadata.creatorTool.value().find(L"Adobe Photoshop CS6") != std::wstring::npos,
|
||||
L"Creator tool should be Photoshop CS6");
|
||||
|
||||
Assert::IsTrue(metadata.documentID.has_value(), L"Document ID should be present");
|
||||
Assert::IsTrue(metadata.documentID.value().find(L"xmp.did:") != std::wstring::npos,
|
||||
L"Document ID should start with xmp.did:");
|
||||
|
||||
Assert::IsTrue(metadata.instanceID.has_value(), L"Instance ID should be present");
|
||||
Assert::IsTrue(metadata.instanceID.value().find(L"xmp.iid:") != std::wstring::npos,
|
||||
L"Instance ID should start with xmp.iid:");
|
||||
|
||||
// Other fields should not be present
|
||||
Assert::IsFalse(metadata.title.has_value(), L"Title should not be present in xmp_test_2.jpg");
|
||||
Assert::IsFalse(metadata.description.has_value(), L"Description should not be present in xmp_test_2.jpg");
|
||||
Assert::IsFalse(metadata.rights.has_value(), L"Rights should not be present in xmp_test_2.jpg");
|
||||
Assert::IsFalse(metadata.creator.has_value(), L"Creator should not be present in xmp_test_2.jpg");
|
||||
}
|
||||
|
||||
TEST_METHOD(ExtractXMP_ClearCache)
|
||||
{
|
||||
// Test cache clearing works
|
||||
WICMetadataExtractor extractor;
|
||||
XMPMetadata metadata;
|
||||
|
||||
std::wstring testFile = GetTestDataPath() + L"\\xmp_test.jpg";
|
||||
|
||||
bool result1 = extractor.ExtractXMPMetadata(testFile, metadata);
|
||||
Assert::IsTrue(result1);
|
||||
|
||||
extractor.ClearCache();
|
||||
|
||||
XMPMetadata metadata2;
|
||||
bool result2 = extractor.ExtractXMPMetadata(testFile, metadata2);
|
||||
Assert::IsTrue(result2);
|
||||
|
||||
// Both calls should succeed
|
||||
Assert::AreEqual(metadata.title.value().c_str(), metadata2.title.value().c_str());
|
||||
}
|
||||
};
|
||||
}
|
||||
45
src/modules/powerrename/unittests/testdata/ATTRIBUTION.md
vendored
Normal file
45
src/modules/powerrename/unittests/testdata/ATTRIBUTION.md
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
# Test Data Attribution
|
||||
|
||||
This directory contains test image files used for PowerRename metadata extraction unit tests. These images are sourced from Wikimedia Commons and are used under the Creative Commons licenses specified below.
|
||||
|
||||
## Test Files and Licenses
|
||||
|
||||
### Files from Carlseibert
|
||||
|
||||
**License:** [Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
|
||||
- `exif_test.jpg` - Uploaded by [Carlseibert](https://commons.wikimedia.org/wiki/File%3AMetadata_test_file_-_includes_data_in_IIM%2C_XMP%2C_and_Exif.jpg) on Wikimedia Commons
|
||||
- `xmp_test.jpg` - Uploaded by [Carlseibert](https://commons.wikimedia.org/wiki/File%3AMetadata_test_file_-_includes_data_in_IIM%2C_XMP%2C_and_Exif.jpg) on Wikimedia Commons
|
||||
|
||||
### Files from Edward Steven
|
||||
|
||||
**License:** [Creative Commons Attribution-ShareAlike 2.0 Generic (CC BY-SA 2.0)](https://creativecommons.org/licenses/by-sa/2.0/)
|
||||
|
||||
- `exif_test_2.jpg` - Uploaded by [Edward Steven](https://commons.wikimedia.org/wiki/File%3AAreca_hutchinsoniana.jpg) on Wikimedia Commons
|
||||
- `xmp_test_2.jpg` - Uploaded by [Edward Steven](https://commons.wikimedia.org/wiki/File%3AAreca_hutchinsoniana.jpg) on Wikimedia Commons
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
We gratefully acknowledge the contributions of Carlseibert and Edward Steven for making these images available under Creative Commons licenses. Their work enables us to test metadata extraction functionality with real-world EXIF and XMP data.
|
||||
|
||||
## Usage
|
||||
|
||||
These test images are used in PowerRename's unit tests to verify correct extraction of:
|
||||
- EXIF metadata (camera make, model, ISO, aperture, shutter speed, etc.)
|
||||
- XMP metadata (creator, title, description, copyright, etc.)
|
||||
- GPS coordinates
|
||||
- Date/time information
|
||||
|
||||
## License Compliance
|
||||
|
||||
These test images are distributed as part of the PowerToys source code repository under their original Creative Commons licenses:
|
||||
- Files from Carlseibert: CC BY-SA 4.0
|
||||
- Files from Edward Steven: CC BY-SA 2.0
|
||||
|
||||
**Modifications:** These images have not been modified from their original versions downloaded from Wikimedia Commons. They are used in their original form for metadata extraction testing purposes.
|
||||
|
||||
**Distribution:** These test images are included in the PowerToys source repository and comply with the terms of their respective Creative Commons licenses through proper attribution in this file. While included in the source code, these images are not distributed in end-user installation packages or releases.
|
||||
|
||||
**Derivatives:** Any modifications or derivative works of these images must comply with the respective CC BY-SA license terms, including proper attribution and applying the same license to the modified versions.
|
||||
|
||||
For more information about Creative Commons licenses, visit: https://creativecommons.org/licenses/
|
||||
BIN
src/modules/powerrename/unittests/testdata/exif_test.jpg
vendored
Normal file
BIN
src/modules/powerrename/unittests/testdata/exif_test.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
BIN
src/modules/powerrename/unittests/testdata/exif_test_2.jpg
vendored
Normal file
BIN
src/modules/powerrename/unittests/testdata/exif_test_2.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 740 KiB |
BIN
src/modules/powerrename/unittests/testdata/xmp_test.jpg
vendored
Normal file
BIN
src/modules/powerrename/unittests/testdata/xmp_test.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
BIN
src/modules/powerrename/unittests/testdata/xmp_test_2.jpg
vendored
Normal file
BIN
src/modules/powerrename/unittests/testdata/xmp_test_2.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 740 KiB |
Reference in New Issue
Block a user