Add glob pattern filtering to playlist library.

Syntax is very similar to .gitignore glob syntax, with a few exceptions to simplify use.
This commit is contained in:
Kai Blaschke
2022-12-04 20:54:15 +01:00
parent a1ffd93e31
commit 9ea98bf01e
13 changed files with 681 additions and 13 deletions

View File

@ -506,4 +506,66 @@ TEST(projectMPlaylistAPI, SetPresetSwitchFailedCallback)
.Times(1);
projectm_playlist_set_preset_switch_failed_event_callback(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), dummyCallback, dummyData);
}
TEST(projectMPlaylistAPI, SetFilter)
{
PlaylistCWrapperMock mockPlaylist;
ProjectM::Playlist::Filter filter;
EXPECT_CALL(mockPlaylist, Filter())
.Times(1)
.WillOnce(ReturnRef(filter));
const char firstFilter[]{"-/some/BadPreset.milk"};
const char secondFilter[]{"+/another/AwesomePreset.milk"};
const char thirdFilter[]{"-/unwanted/Preset.milk"};
const char* filterList[]{firstFilter, secondFilter, thirdFilter};
projectm_playlist_set_filter(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), filterList, 3);
const auto& internalFilterList = filter.List();
ASSERT_EQ(internalFilterList.size(), 3);
EXPECT_EQ(internalFilterList.at(0), "-/some/BadPreset.milk");
EXPECT_EQ(internalFilterList.at(1), "+/another/AwesomePreset.milk");
EXPECT_EQ(internalFilterList.at(2), "-/unwanted/Preset.milk");
}
TEST(projectMPlaylistAPI, GetFilter)
{
PlaylistCWrapperMock mockPlaylist;
ProjectM::Playlist::Filter filter;
filter.SetList({"-/some/BadPreset.milk",
"+/another/AwesomePreset.milk",
"-/unwanted/Preset.milk"});
EXPECT_CALL(mockPlaylist, Filter())
.Times(1)
.WillOnce(ReturnRef(filter));
size_t count{};
auto filterList = projectm_playlist_get_filter(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), &count);
ASSERT_EQ(count, 3);
ASSERT_NE(filterList, nullptr);
EXPECT_STREQ(filterList[0], "-/some/BadPreset.milk");
EXPECT_STREQ(filterList[1], "+/another/AwesomePreset.milk");
EXPECT_STREQ(filterList[2], "-/unwanted/Preset.milk");
projectm_playlist_free_string_array(filterList);
}
TEST(projectMPlaylistAPI, ApplyFilter)
{
PlaylistCWrapperMock mockPlaylist;
EXPECT_CALL(mockPlaylist, ApplyFilter())
.Times(1)
.WillOnce(Return(5));
EXPECT_EQ(projectm_playlist_apply_filter(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist)), 5);
}

View File

@ -7,11 +7,10 @@ find_package(GTest 1.10 REQUIRED NO_MODULE)
add_executable(projectM-playlist-unittest
APITest.cpp
ItemTest.cpp
PlaylistCWrapperMock.cpp
PlaylistCWrapperMock.h
PlaylistTest.cpp
ProjectMAPIMocks.cpp
)
FilterTest.cpp)
target_compile_definitions(projectM-playlist-unittest
PRIVATE

View File

@ -0,0 +1,180 @@
#include <Filter.hpp>
#include <gtest/gtest.h>
TEST(projectMPlaylistFilter, List)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-TestString.milk",
"+AnotherTestString*"});
const auto& filters = filter.List();
ASSERT_EQ(filters.size(), 2);
EXPECT_EQ(filters.at(0), "-TestString.milk");
EXPECT_EQ(filters.at(1), "+AnotherTestString*");
}
TEST(projectMPlaylistFilter, ExactMatchExclude)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-TestString.milk"});
EXPECT_FALSE(filter.Passes("TestString.milk"));
EXPECT_TRUE(filter.Passes("Teststring.milk"));
}
TEST(projectMPlaylistFilter, ExactMatchExcludePath)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-/path/to/TestString.milk"});
EXPECT_FALSE(filter.Passes("/path/to/TestString.milk"));
EXPECT_TRUE(filter.Passes("/path/to/Teststring.milk"));
}
TEST(projectMPlaylistFilter, SingleCharacterExclude)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-/path/to/TestStr?ng.milk"});
EXPECT_FALSE(filter.Passes("/path/to/TestString.milk"));
EXPECT_FALSE(filter.Passes("/path/to/TestStrung.milk"));
EXPECT_TRUE(filter.Passes("/path/to/TestStr/ng.milk"));
EXPECT_TRUE(filter.Passes("/path/to/Teststring.milk"));
}
TEST(projectMPlaylistFilter, MultiCharacterExclude)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-/path/to/Test*.milk"});
EXPECT_FALSE(filter.Passes("/path/to/TestString.milk"));
EXPECT_FALSE(filter.Passes("/path/to/TestFile.milk"));
EXPECT_FALSE(filter.Passes("/path/to/TestALotOfAdditional.Characters.milk"));
EXPECT_FALSE(filter.Passes("/path/to/Test.milk"));
EXPECT_TRUE(filter.Passes("/path/to/Test/String.milk"));
}
TEST(projectMPlaylistFilter, MultiCharacterExcludeExamples)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-a"});
EXPECT_FALSE(filter.Passes("a"));
EXPECT_FALSE(filter.Passes("x/a"));
EXPECT_FALSE(filter.Passes("x/y/a"));
EXPECT_TRUE(filter.Passes("b"));
EXPECT_TRUE(filter.Passes("x/b"));
EXPECT_TRUE(filter.Passes("a/a/b"));
filter.SetList({"-/*"});
EXPECT_FALSE(filter.Passes("a"));
EXPECT_FALSE(filter.Passes("b"));
EXPECT_TRUE(filter.Passes("x/a"));
EXPECT_TRUE(filter.Passes("x/b"));
EXPECT_TRUE(filter.Passes("x/y/a"));
filter.SetList({"-/a"});
EXPECT_FALSE(filter.Passes("a"));
EXPECT_FALSE(filter.Passes("/a"));
EXPECT_FALSE(filter.Passes("./a"));
EXPECT_TRUE(filter.Passes("x/a"));
EXPECT_TRUE(filter.Passes("x/y/a"));
}
TEST(projectMPlaylistFilter, PathGlobExclude)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-**/Test.milk"});
EXPECT_FALSE(filter.Passes("/path/to/Test.milk"));
EXPECT_FALSE(filter.Passes("/path/Test.milk"));
EXPECT_FALSE(filter.Passes("Test.milk"));
EXPECT_FALSE(filter.Passes("\\path\\to\\path\\to\\path\\to/path/to/path/to/Test.milk"));
EXPECT_TRUE(filter.Passes("/path/to/Test/.milk"));
}
TEST(projectMPlaylistFilter, PathGlobExcludeExamples)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-**/a"});
EXPECT_FALSE(filter.Passes("a"));
EXPECT_FALSE(filter.Passes("x/a"));
EXPECT_FALSE(filter.Passes("x/y/a"));
EXPECT_TRUE(filter.Passes("b"));
EXPECT_TRUE(filter.Passes("x/b"));
filter.SetList({"-a/**/b"});
EXPECT_FALSE(filter.Passes("a/b"));
EXPECT_FALSE(filter.Passes("a/x/b"));
EXPECT_FALSE(filter.Passes("a/x/y/b"));
EXPECT_TRUE(filter.Passes("x/a/b"));
EXPECT_TRUE(filter.Passes("a/b/x"));
filter.SetList({"-a/**"});
EXPECT_FALSE(filter.Passes("a/x"));
EXPECT_FALSE(filter.Passes("a/y"));
EXPECT_FALSE(filter.Passes("a/x/y"));
EXPECT_TRUE(filter.Passes("a"));
EXPECT_TRUE(filter.Passes("b/x"));
}
TEST(projectMPlaylistFilter, LargeGlobs)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a"});
EXPECT_FALSE(filter.Passes("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
EXPECT_TRUE(filter.Passes("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"));
filter.SetList({"-/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a"});
EXPECT_FALSE(filter.Passes("/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a"));
EXPECT_TRUE(filter.Passes("/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b"));
}
TEST(projectMPlaylistFilter, MultipleFilters)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-/path/to/Test*.milk",
"/path/to/another\\Test*.milk",
"+/path/to/yet/another\\Test*.milk",
"-Test*.milk"});
EXPECT_FALSE(filter.Passes("/path/to/TestSome.milk"));
EXPECT_FALSE(filter.Passes("/path/to/another/TestSome.milk"));
EXPECT_TRUE(filter.Passes("\\path\\to\\yet\\another\\TestCase.milk"));
EXPECT_FALSE(filter.Passes("/path/of/my/TestPreset.milk"));
EXPECT_TRUE(filter.Passes("/another/something/completely/different"));
}
TEST(projectMPlaylistFilter, MatchEverything)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-/**"});
EXPECT_FALSE(filter.Passes("/path/to/TestSome.milk"));
EXPECT_FALSE(filter.Passes("/path/to/another/TestSome.milk"));
EXPECT_FALSE(filter.Passes("\\path\\to\\yet\\another\\TestCase.milk"));
EXPECT_FALSE(filter.Passes("/path/of/my/TestPreset.milk"));
EXPECT_FALSE(filter.Passes("/another/something/completely/different"));
}

View File

@ -1 +0,0 @@
#include "PlaylistCWrapperMock.h"

View File

@ -33,4 +33,6 @@ public:
MOCK_METHOD(void, PlayPresetIndex, (size_t, bool, bool) );
MOCK_METHOD(void, RemoveLastHistoryEntry, ());
MOCK_METHOD(void, SetPresetSwitchFailedCallback, (projectm_playlist_preset_switch_failed_event, void*));
MOCK_METHOD(class ProjectM::Playlist::Filter&, Filter, ());
MOCK_METHOD(size_t, ApplyFilter, ());
};

View File

@ -627,3 +627,50 @@ TEST(projectMPlaylistPlaylist, RemoveLastHistoryEntry)
EXPECT_EQ(playlist.LastPresetIndex(), 0);
}
TEST(projectMPlaylistPlaylist, AddItemWithFilter)
{
Playlist playlist;
playlist.Filter().SetList({"-/**/Preset*.milk"});
EXPECT_FALSE(playlist.AddItem("/some/PresetZ.milk", Playlist::InsertAtEnd, false));
EXPECT_FALSE(playlist.AddItem("/some/PresetA.milk", Playlist::InsertAtEnd, false));
EXPECT_FALSE(playlist.AddItem("/some/other/PresetC.milk", Playlist::InsertAtEnd, false));
EXPECT_TRUE(playlist.AddItem("/some/MyFavorite.milk", Playlist::InsertAtEnd, false));
ASSERT_EQ(playlist.Size(), 1);
}
TEST(projectMPlaylistPlaylist, AddPathWithFilter)
{
Playlist playlist;
playlist.Filter().SetList({"-**/presets/Test_*.milk"});
EXPECT_EQ(playlist.AddPath(PROJECTM_PLAYLIST_TEST_DATA_DIR "/presets", 0, true, false), 1);
ASSERT_EQ(playlist.Size(), 1);
}
TEST(projectMPlaylistPlaylist, ApplyFilter)
{
Playlist playlist;
// Remove Test_A on load
playlist.Filter().SetList({"-Test_A.milk"});
EXPECT_EQ(playlist.AddPath(PROJECTM_PLAYLIST_TEST_DATA_DIR "/presets", 0, true, false), 3);
ASSERT_EQ(playlist.Size(), 3);
// Apply new filter that only removes Test_B
playlist.Filter().SetList({"-Test_B.milk"});
EXPECT_EQ(playlist.ApplyFilter(), 1);
// Test_A will not reappear.
ASSERT_EQ(playlist.Size(), 2);
}