From 9ea98bf01e484340d096fcba9a8d35fac6c351ca Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Sun, 4 Dec 2022 20:54:15 +0100 Subject: [PATCH] Add glob pattern filtering to playlist library. Syntax is very similar to .gitignore glob syntax, with a few exceptions to simplify use. --- src/playlist/CMakeLists.txt | 2 + src/playlist/Filter.cpp | 159 +++++++++++++++++++++ src/playlist/Filter.hpp | 53 +++++++ src/playlist/Playlist.cpp | 55 ++++++-- src/playlist/Playlist.hpp | 19 ++- src/playlist/PlaylistCWrapper.cpp | 48 +++++++ src/playlist/playlist.h | 63 +++++++++ tests/playlist/APITest.cpp | 62 ++++++++ tests/playlist/CMakeLists.txt | 3 +- tests/playlist/FilterTest.cpp | 180 ++++++++++++++++++++++++ tests/playlist/PlaylistCWrapperMock.cpp | 1 - tests/playlist/PlaylistCWrapperMock.h | 2 + tests/playlist/PlaylistTest.cpp | 47 +++++++ 13 files changed, 681 insertions(+), 13 deletions(-) create mode 100644 src/playlist/Filter.cpp create mode 100644 src/playlist/Filter.hpp create mode 100644 tests/playlist/FilterTest.cpp delete mode 100644 tests/playlist/PlaylistCWrapperMock.cpp diff --git a/src/playlist/CMakeLists.txt b/src/playlist/CMakeLists.txt index 488322900..4565c2a45 100644 --- a/src/playlist/CMakeLists.txt +++ b/src/playlist/CMakeLists.txt @@ -3,6 +3,8 @@ if(NOT ENABLE_PLAYLIST) endif() add_library(projectM_playlist STATIC + Filter.cpp + Filter.hpp Item.cpp Item.hpp Playlist.cpp diff --git a/src/playlist/Filter.cpp b/src/playlist/Filter.cpp new file mode 100644 index 000000000..ce56de5ac --- /dev/null +++ b/src/playlist/Filter.cpp @@ -0,0 +1,159 @@ +#include "Filter.hpp" + +#include + +namespace ProjectM { +namespace Playlist { + +auto Filter::List() const -> const std::vector& +{ + return m_filters; +} + +void Filter::SetList(std::vector filterList) +{ + m_filters = std::move(filterList); +} + + +auto Filter::Passes(const std::string& filename) -> bool +{ + for (const auto& filterExpression : m_filters) + { + if (!filterExpression.empty() && ApplyExpression(filename, filterExpression)) + { + // Default action is "remove if filename matches". + return filterExpression.at(0) == '+'; + } + } + + return true; +} + + +auto Filter::ApplyExpression(const std::string& filename, const std::string& filterExpression) -> bool +{ + // Implementation idea thanks to Robert van Engelen + // https://www.codeproject.com/Articles/5163931/Fast-String-Matching-with-Wildcards-Globs-and-Giti + + if (filename.empty() || filterExpression.empty()) + { + return false; + } + + const auto* currentFilenameChar{filename.c_str()}; + const auto* currentFilterChar{filterExpression.c_str()}; + + const char* previousFilenameChar{nullptr}; + const char* previousFilterChar{nullptr}; + + bool inPathglob{false}; //!< True if the glob has a '**' pattern + + auto isPathSep = [](const char* character) { + return *character == '/' || *character == '\\'; + }; + + if (*currentFilterChar == '+' || *currentFilterChar == '-') + { + currentFilterChar++; + } + + if (isPathSep(currentFilterChar)) + { + while (*currentFilenameChar == '.' && isPathSep(¤tFilenameChar[1])) + { + currentFilenameChar += 2; + } + while (isPathSep(currentFilenameChar)) + { + currentFilenameChar++; + } + currentFilterChar++; + } + else if (strchr(currentFilterChar, '/') == nullptr && strchr(currentFilterChar, '\\') == nullptr) + { + const auto *separatorUnix = strrchr(currentFilenameChar, '/'); + const auto *separatorwindows = strrchr(currentFilenameChar, '\\'); + if (separatorUnix != nullptr && separatorwindows != nullptr) + { + currentFilenameChar = std::min(separatorUnix, separatorwindows) + 1; + } + else if (separatorUnix != nullptr) + { + currentFilenameChar = separatorUnix + 1; + } + else if (separatorwindows != nullptr) + { + currentFilenameChar = separatorwindows + 1; + } + } + + while (*currentFilenameChar != '\0') + { + switch (*currentFilterChar) + { + case '*': + previousFilenameChar = currentFilenameChar; + previousFilterChar = currentFilterChar; + + inPathglob = false; + currentFilterChar++; + if (*currentFilterChar == '*') + { + currentFilterChar++; + if (*currentFilterChar == '\0') + { + return true; + } + if (!isPathSep(currentFilterChar)) + { + return false; + } + + inPathglob = true; + currentFilterChar++; + } + + continue; + + case '?': + if (isPathSep(currentFilenameChar)) + { + break; + } + + currentFilenameChar++; + currentFilterChar++; + + default: + if (*currentFilterChar != *currentFilenameChar && + !(isPathSep(currentFilterChar) && isPathSep(currentFilenameChar))) + { + break; + } + currentFilenameChar++; + currentFilterChar++; + continue; + } + + if (previousFilterChar != nullptr && (inPathglob || !isPathSep(previousFilenameChar))) + { + currentFilenameChar = ++previousFilenameChar; + currentFilterChar = previousFilterChar; + continue; + } + + return false; + } + + while (*currentFilterChar == '*') + { + currentFilterChar++; + } + + return *currentFilterChar == '\0'; +} + + +} // namespace Playlist +} // namespace ProjectM diff --git a/src/playlist/Filter.hpp b/src/playlist/Filter.hpp new file mode 100644 index 000000000..a84550266 --- /dev/null +++ b/src/playlist/Filter.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include + +namespace ProjectM { +namespace Playlist { + +/** + * @brief Implements a simple filename globbing filter. + * + * See API docs of projectm_playlist_set_filter() in playlist.h for syntax details. + */ +class Filter +{ +public: + /** + * @brief Returns the filter list. + * @return The current filter list. + */ + auto List() const -> const std::vector&; + + /** + * @brief Sets the filter list. + * @param filterList The new filter list. + */ + void SetList(std::vector filterList); + + /** + * @brief Applies the current filter list to the filename. + * + * This will apply all rules in order, and return true if the filename should be included + * in the playlist. If no rule matches or the filter list is empty, the filename will pass. + * + * @param filename The filename to check. + * @return True if the filename passes the filter, false if it should b skipped. + */ + auto Passes(const std::string& filename) ->bool; + +private: + /** + * @brief Applies a single filter to the given filename. + * @param character The filename to check. + * @param filterExpression The filter expression. A leading + or - is ignored. + * @return True if the filter matches the filename, false otherwise. + */ + static auto ApplyExpression(const std::string& character, const std::string& filterExpression) -> bool; + + std::vector m_filters; //!< List of filters to apply. +}; + +} // namespace Playlist +} // namespace ProjectM diff --git a/src/playlist/Playlist.cpp b/src/playlist/Playlist.cpp index 557c82e4f..8c83d211f 100644 --- a/src/playlist/Playlist.cpp +++ b/src/playlist/Playlist.cpp @@ -52,6 +52,11 @@ bool Playlist::AddItem(const std::string& filename, uint32_t index, bool allowDu return false; } + if (!m_filter.Passes(filename)) + { + return false; + } + if (!allowDuplicates) { if (std::find(m_items.begin(), m_items.end(), filename) != m_items.end()) @@ -301,6 +306,47 @@ auto Playlist::SetPresetIndex(size_t presetIndex) -> size_t } +void Playlist::RemoveLastHistoryEntry() +{ + if (!m_presetHistory.empty()) + { + m_presetHistory.pop_back(); + } +} + + +auto Playlist::Filter() -> class Filter& +{ + return m_filter; +} + + +auto Playlist::ApplyFilter() -> size_t +{ + size_t itemsRemoved{}; + + for (auto it = begin(m_items); it != end(m_items);) + { + if (!m_filter.Passes(it->Filename())) + { + it = m_items.erase(it); + itemsRemoved++; + } + else + { + ++it; + } + } + + if (itemsRemoved != 0) + { + m_presetHistory.clear(); + } + + return itemsRemoved; +} + + void Playlist::AddCurrentPresetIndexToHistory() { // No duplicate entries. @@ -317,14 +363,5 @@ void Playlist::AddCurrentPresetIndexToHistory() } -void Playlist::RemoveLastHistoryEntry() -{ - if (!m_presetHistory.empty()) - { - m_presetHistory.pop_back(); - } -} - - } // namespace Playlist } // namespace ProjectM diff --git a/src/playlist/Playlist.hpp b/src/playlist/Playlist.hpp index 95fd0f329..38fb9bc72 100644 --- a/src/playlist/Playlist.hpp +++ b/src/playlist/Playlist.hpp @@ -1,5 +1,6 @@ #pragma once +#include "Filter.hpp" #include "Item.hpp" #include @@ -225,13 +226,29 @@ public: */ virtual void RemoveLastHistoryEntry(); + /** + * @brief Returns the current playlist filter list. + * @return The filter list for the current playlist. + */ + virtual auto Filter() -> class Filter&; + + /** + * @brief Applies the current filter list to the existing playlist. + * + * Note this function only removes items. Previously filtered items are not added again. + * + * @return The number of filtered (removed) items. + */ + virtual auto ApplyFilter() -> size_t; + private: /** * @brief Adds a preset to the history and trims the list if it gets too long. */ void AddCurrentPresetIndexToHistory(); - std::vector m_items; //!< Items in the current playlist. + std::vector m_items; //!< All items in the current playlist. + class Filter m_filter; //!< Item filter. bool m_shuffle{false}; //!< True if shuffle mode is enabled, false to play presets in order. size_t m_currentPosition{0}; //!< Current playlist position. std::list m_presetHistory; //!< The playback history. diff --git a/src/playlist/PlaylistCWrapper.cpp b/src/playlist/PlaylistCWrapper.cpp index e62755e0f..b4dbb88cf 100644 --- a/src/playlist/PlaylistCWrapper.cpp +++ b/src/playlist/PlaylistCWrapper.cpp @@ -464,6 +464,7 @@ uint32_t projectm_playlist_play_previous(projectm_playlist_handle instance, bool } } + uint32_t projectm_playlist_play_last(projectm_playlist_handle instance, bool hard_cut) { auto* playlist = playlist_handle_to_instance(instance); @@ -477,4 +478,51 @@ uint32_t projectm_playlist_play_last(projectm_playlist_handle instance, bool har { return 0; } +} + + +void projectm_playlist_set_filter(projectm_playlist_handle instance, const char** filter_list, + size_t count) +{ + auto* playlist = playlist_handle_to_instance(instance); + + std::vector filterList; + for (size_t index = 0; index < count; index++) + { + if (filter_list[index] == nullptr) + { + continue; + } + filterList.emplace_back(filter_list[index]); + } + + playlist->Filter().SetList(filterList); +} + + +auto projectm_playlist_get_filter(projectm_playlist_handle instance, size_t* count) -> char** +{ + auto* playlist = playlist_handle_to_instance(instance); + + const auto& filterList = playlist->Filter().List(); + + auto* array = new char* [filterList.size() + 1] {}; + + int index{0}; + for (const auto& filter : filterList) + { + array[index] = new char[filter.length() + 1]{}; + filter.copy(array[index], filter.length()); + index++; + } + + *count = filterList.size(); + return array; +} + + +auto projectm_playlist_apply_filter(projectm_playlist_handle instance) -> size_t +{ + auto* playlist = playlist_handle_to_instance(instance); + return playlist->ApplyFilter(); } \ No newline at end of file diff --git a/src/playlist/playlist.h b/src/playlist/playlist.h index 74d7984d9..35fa08044 100644 --- a/src/playlist/playlist.h +++ b/src/playlist/playlist.h @@ -419,6 +419,69 @@ uint32_t projectm_playlist_play_previous(projectm_playlist_handle instance, bool */ uint32_t projectm_playlist_play_last(projectm_playlist_handle instance, bool hard_cut); +/** + * @brief Sets a new filter list. + * + *

Does not immediately apply the new list to an existing playlist, only newly added files + * will be affected. If you need to filter the existing playlist after calling this method, + * additionally call projectm_playlist_apply_filter() afterwards.

+ * + *

The filter list consists of simple globbing expressions similar to the .gitignore syntax:

+ * + *
    + *
  • ?: Matches any single character except /.
  • + *
  • *: Matches 0 or more characters except /.
  • + *
  • /: When used at the begin of a glob, matches if + * pathname has no path separator.
  • + *
  • **‍/: Matches 0 or more directories.
  • + *
  • /‍**: When at the end of the glob, matches everything after the /.
  • + *
+ * + *

In globbing expressions, \\ can be used as path separator instead of /. The backslash can't + * be used to escape globbing patterns, so matching literal * and ? in filenames is not possible. + * This is not a huge issue as Windows doesn't allow those characters in filenames and Milkdrop + * files originate from the Windows world. Character classes like "[0-9]" are also not supported to + * keep the syntax simple.

+ * + *

Each line can be prefixed with either + or - to either include files matching the pattern + * or excluding them. Any other character is not interpreted as a prefix and the filter line is + * matching as an exclude filter. To match a literal + or - at the beginning, add the appropriate + * prefix in front. Empty filters never match anything, even if the filename is empty.

+ * + *

The filter list is checked in order. The first pattern that matches the filename determines + * the filter result (include or exclude). If no pattern matches, the file is included. In the case + * that a default exclude action is required, add a "-/‍**" filter at the end of the list.

+ * + * @param instance The playlist manager instance. + * @param filter_list An array with filter strings. + * @param count The size of the filter array. + */ +void projectm_playlist_set_filter(projectm_playlist_handle instance, const char** filter_list, + size_t count); + +/** + * @brief Returns the current filter list. + * + * Always call projectm_playlist_free_string_array() on the returned pointer if the data is + * no longer needed. + * + * @param instance The playlist manager instance. + * @param[out] count The size of the filter array. + * @return An array with filter strings. + */ +char** projectm_playlist_get_filter(projectm_playlist_handle instance, size_t* count); + +/** + * @brief Applies the current filter list to the existing playlist. + * + * Note this function only removes items. Previously filtered items are not added again. If items + * were removed, the playback history is cleared. + * + * @param instance The playlist manager instance. + * @return The number of removed items. + */ +size_t projectm_playlist_apply_filter(projectm_playlist_handle instance); + #ifdef __cplusplus } // extern "C" diff --git a/tests/playlist/APITest.cpp b/tests/playlist/APITest.cpp index 0d6e030d9..37aa89fda 100644 --- a/tests/playlist/APITest.cpp +++ b/tests/playlist/APITest.cpp @@ -506,4 +506,66 @@ TEST(projectMPlaylistAPI, SetPresetSwitchFailedCallback) .Times(1); projectm_playlist_set_preset_switch_failed_event_callback(reinterpret_cast(&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(&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(&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(&mockPlaylist)), 5); } \ No newline at end of file diff --git a/tests/playlist/CMakeLists.txt b/tests/playlist/CMakeLists.txt index db1ba0a3e..d17277a97 100644 --- a/tests/playlist/CMakeLists.txt +++ b/tests/playlist/CMakeLists.txt @@ -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 diff --git a/tests/playlist/FilterTest.cpp b/tests/playlist/FilterTest.cpp new file mode 100644 index 000000000..12c64862e --- /dev/null +++ b/tests/playlist/FilterTest.cpp @@ -0,0 +1,180 @@ +#include + +#include + +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")); +} \ No newline at end of file diff --git a/tests/playlist/PlaylistCWrapperMock.cpp b/tests/playlist/PlaylistCWrapperMock.cpp deleted file mode 100644 index c98ada727..000000000 --- a/tests/playlist/PlaylistCWrapperMock.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "PlaylistCWrapperMock.h" diff --git a/tests/playlist/PlaylistCWrapperMock.h b/tests/playlist/PlaylistCWrapperMock.h index 49aa90bb0..68ff50f5a 100644 --- a/tests/playlist/PlaylistCWrapperMock.h +++ b/tests/playlist/PlaylistCWrapperMock.h @@ -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, ()); }; diff --git a/tests/playlist/PlaylistTest.cpp b/tests/playlist/PlaylistTest.cpp index 8d20a29f0..87a000c7b 100644 --- a/tests/playlist/PlaylistTest.cpp +++ b/tests/playlist/PlaylistTest.cpp @@ -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); +}