diff --git a/src/libs/services/recommendation/impl/audio-similarity/AudioSimilarityEngine.impl.hpp b/src/libs/services/recommendation/impl/audio-similarity/AudioSimilarityEngine.impl.hpp index c3c869c7..114335ef 100644 --- a/src/libs/services/recommendation/impl/audio-similarity/AudioSimilarityEngine.impl.hpp +++ b/src/libs/services/recommendation/impl/audio-similarity/AudioSimilarityEngine.impl.hpp @@ -50,10 +50,10 @@ #include "InterpolationFitConstraint.hpp" #include "MaxDistanceConstraint.hpp" -#include "NearDuplicateEmbeddingConstraint.hpp" #include "SmoothTransitionConstraint.hpp" #include "track-selection-constraints/DuplicateTrackConstraint.hpp" #include "track-selection-constraints/SameArtistConstraint.hpp" +#include "track-selection-constraints/SameRecordingMBIDConstraint.hpp" #include "track-selection-constraints/SameReleaseConstraint.hpp" #include "Types.hpp" @@ -121,11 +121,9 @@ namespace lms::recommendation constexpr float smoothTransitionWeight{ 0.2F }; constexpr float sameReleaseWeight{ 0.5F }; constexpr float sameArtistWeight{ 0.5F }; - constexpr float nearDuplicateThreshold{ 0.01F }; - _similarityEvaluator = {}; _similarityEvaluator.addHardConstraint(std::make_unique()); - _similarityEvaluator.addHardConstraint(std::make_unique>(_trackVectors, nearDuplicateThreshold)); + _similarityEvaluator.addHardConstraint(std::make_unique(_trackMetadata)); _similarityEvaluator.addHardConstraint(std::make_unique>(_trackVectors, _trackDistanceThreshold)); _similarityEvaluator.addSoftConstraint(std::make_unique>(_trackVectors), interpolationFitWeight); _similarityEvaluator.addSoftConstraint(std::make_unique>(_trackVectors), smoothTransitionWeight); @@ -134,7 +132,7 @@ namespace lms::recommendation _pathEvaluator = {}; _pathEvaluator.addHardConstraint(std::make_unique()); - _pathEvaluator.addHardConstraint(std::make_unique>(_trackVectors, nearDuplicateThreshold)); + _pathEvaluator.addHardConstraint(std::make_unique(_trackMetadata)); _pathEvaluator.addSoftConstraint(std::make_unique>(_trackVectors), interpolationFitWeight); _pathEvaluator.addSoftConstraint(std::make_unique>(_trackVectors), smoothTransitionWeight); _pathEvaluator.addSoftConstraint(std::make_unique(_trackMetadata), sameReleaseWeight); @@ -595,28 +593,29 @@ namespace lms::recommendation _trackVectors.try_emplace(trackId, &reducedVector); }); - db::Release::find(session, db::Release::FindParameters{}, [&](const db::Release::pointer& release) { - std::vector> releaseTrackFeatures; + db::Track::find(session, db::Track::FindParameters{}, [&](const db::Track::pointer& track) { + const auto itVec{ _trackVectors.find(track->getId()) }; + if (itVec == _trackVectors.cend()) + return; - db::Track::FindParameters params; - params.setRelease(release->getId()); + auto& meta{ _trackMetadata[track->getId()] }; + const db::ReleaseId releaseId{ track->getReleaseId() }; + meta.releaseId = releaseId; + meta.recordingMBID = track->getRecordingMBID(); - const auto trackIds{ db::Track::findIds(session, params) }; - for (const db::TrackId trackId : trackIds.results) - { - const auto itFeatures{ _trackVectors.find(trackId) }; - if (itFeatures != std::cend(_trackVectors)) - { - assert(itFeatures->second); - releaseTrackFeatures.emplace_back(*itFeatures->second); - _trackMetadata[trackId].releaseId = release->getId(); - } - } - - if (!releaseTrackFeatures.empty()) - _releaseVectors.try_emplace(release->getId(), std::move(releaseTrackFeatures)); + if (releaseId.isValid()) + _releaseVectors[releaseId].emplace_back(*itVec->second); }); + math::MedoidCalculator calc; + for (const auto& [id, vecs] : _releaseVectors) + { + calc.clear(); + for (const auto& v : vecs) + calc.add(v.get()); + _releaseMedoids.try_emplace(id, calc.finalize()); + } + db::Artist::find(session, db::Artist::FindParameters{}, [&](const db::Artist::pointer& artist) { const auto mbid{ artist->getMBID() }; // skip "Various Artists" to avoid false artist matches @@ -658,15 +657,6 @@ namespace lms::recommendation _artistVectors.try_emplace(artist->getId(), std::move(artistTrackVectors)); }); - math::MedoidCalculator calc; - for (const auto& [id, vecs] : _releaseVectors) - { - calc.clear(); - for (const auto& v : vecs) - calc.add(v.get()); - _releaseMedoids.try_emplace(id, calc.finalize()); - } - for (const auto& [id, vecs] : _artistVectors) { calc.clear(); diff --git a/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp b/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp index 5e4476a8..8c01c15c 100644 --- a/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp +++ b/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp @@ -23,7 +23,6 @@ #include #include #include -#include #include #include "database/IDb.hpp" @@ -39,6 +38,7 @@ #include "track-selection-constraints/DuplicateTrackConstraint.hpp" #include "track-selection-constraints/SameArtistConstraint.hpp" +#include "track-selection-constraints/SameRecordingMBIDConstraint.hpp" #include "track-selection-constraints/SameReleaseConstraint.hpp" #include "track-selection-constraints/TrackCandidateContext.hpp" @@ -103,6 +103,7 @@ namespace lms::recommendation constexpr float sameReleaseWeight{ 0.5F }; constexpr float sameArtistWeight{ 0.5F }; _trackEvaluator.addHardConstraint(std::make_unique()); + _trackEvaluator.addHardConstraint(std::make_unique(_trackMetadata)); _trackEvaluator.addSoftConstraint(std::make_unique(_trackMetadata), sameReleaseWeight); _trackEvaluator.addSoftConstraint(std::make_unique(_trackMetadata), sameArtistWeight); } @@ -122,8 +123,8 @@ namespace lms::recommendation db::Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; - buildTrackMetadata(session); buildTrackClusters(session); + buildTrackMetadata(session); buildReleaseClusters(); buildArtistClusters(); @@ -134,11 +135,17 @@ namespace lms::recommendation { LOG(DEBUG, "building track metadata..."); - db::Release::find(session, db::Release::FindParameters{}, [&](const db::Release::pointer& release) { - db::Track::FindParameters params; - params.setRelease(release->getId()); - for (const db::TrackId trackId : db::Track::findIds(session, params).results) - _trackMetadata[trackId].releaseId = release->getId(); + // Ensure cluster tracks with no release/artist have an entry + for (const auto& [trackId, _] : _trackClusters) + _trackMetadata.try_emplace(trackId); + + db::Track::find(session, db::Track::FindParameters{}, [&](const db::Track::pointer& track) { + const auto it{ _trackMetadata.find(track->getId()) }; + if (it != _trackMetadata.cend()) + { + it->second.releaseId = track->getReleaseId(); + it->second.recordingMBID = track->getRecordingMBID(); + } }); db::Artist::find(session, db::Artist::FindParameters{}, [&](const db::Artist::pointer& artist) { diff --git a/src/libs/services/recommendation/impl/clusters/ClustersEngine.hpp b/src/libs/services/recommendation/impl/clusters/ClustersEngine.hpp index f123c15e..bd46f9ac 100644 --- a/src/libs/services/recommendation/impl/clusters/ClustersEngine.hpp +++ b/src/libs/services/recommendation/impl/clusters/ClustersEngine.hpp @@ -54,8 +54,8 @@ namespace lms::recommendation ArtistResults findSimilarArtists(db::ArtistId artistId, core::EnumSet linkTypes, std::size_t maxCount) const override; TrackResults greedySelect(std::vector candidates, std::vector selectedTracks, std::size_t maxCount) const; - void buildTrackMetadata(db::Session& session); void buildTrackClusters(db::Session& session); + void buildTrackMetadata(db::Session& session); void buildReleaseClusters(); void buildArtistClusters(); diff --git a/src/libs/services/recommendation/impl/track-selection-constraints/SameRecordingMBIDConstraint.hpp b/src/libs/services/recommendation/impl/track-selection-constraints/SameRecordingMBIDConstraint.hpp new file mode 100644 index 00000000..9a76edd7 --- /dev/null +++ b/src/libs/services/recommendation/impl/track-selection-constraints/SameRecordingMBIDConstraint.hpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2026 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "ITrackCandidateHardConstraint.hpp" +#include "TrackMetadata.hpp" + +namespace lms::recommendation +{ + class SameRecordingMBIDConstraint : public ITrackCandidateHardConstraint + { + public: + SameRecordingMBIDConstraint(const TrackMetadataMap& trackMetadata) + : _trackMetadata{ trackMetadata } + { + } + + bool rejects(const TrackCandidateContext& context) const override + { + const auto it{ _trackMetadata.find(context.candidateTrackId) }; + if (it == _trackMetadata.cend() || !it->second.recordingMBID) + return false; + + for (const db::TrackId selectedId : context.selectedTracks) + { + const auto itSel{ _trackMetadata.find(selectedId) }; + if (itSel != _trackMetadata.cend() && itSel->second.recordingMBID == it->second.recordingMBID) + return true; + } + return false; + } + + private: + const TrackMetadataMap& _trackMetadata; + }; +} // namespace lms::recommendation diff --git a/src/libs/services/recommendation/impl/track-selection-constraints/TrackMetadata.hpp b/src/libs/services/recommendation/impl/track-selection-constraints/TrackMetadata.hpp index d2a89b43..88393b7a 100644 --- a/src/libs/services/recommendation/impl/track-selection-constraints/TrackMetadata.hpp +++ b/src/libs/services/recommendation/impl/track-selection-constraints/TrackMetadata.hpp @@ -19,9 +19,11 @@ #pragma once +#include #include #include +#include "core/UUID.hpp" #include "database/objects/ArtistId.hpp" #include "database/objects/ReleaseId.hpp" #include "database/objects/TrackId.hpp" @@ -30,9 +32,9 @@ namespace lms::recommendation { struct TrackMetadata { - db::ReleaseId releaseId; // invalid if track has no release - std::vector artistIds; // sorted; album artists only - // Future: db::MediaLibraryId mediaLibraryId; + db::ReleaseId releaseId; // invalid if track has no release + std::vector artistIds; // sorted, album artists only + std::optional recordingMBID; // absent if track has no recording MBID }; using TrackMetadataMap = std::unordered_map; diff --git a/src/libs/services/recommendation/test/ConstraintsTest.cpp b/src/libs/services/recommendation/test/ConstraintsTest.cpp index 4d9cc9b9..f9fc7b38 100644 --- a/src/libs/services/recommendation/test/ConstraintsTest.cpp +++ b/src/libs/services/recommendation/test/ConstraintsTest.cpp @@ -24,390 +24,474 @@ #include "database/objects/TrackId.hpp" #include "math/Vector.hpp" +#include "core/UUID.hpp" + #include "audio-similarity/InterpolationFitConstraint.hpp" #include "audio-similarity/MaxDistanceConstraint.hpp" #include "audio-similarity/NearDuplicateEmbeddingConstraint.hpp" #include "audio-similarity/SmoothTransitionConstraint.hpp" #include "track-selection-constraints/DuplicateTrackConstraint.hpp" #include "track-selection-constraints/SameArtistConstraint.hpp" +#include "track-selection-constraints/SameRecordingMBIDConstraint.hpp" #include "track-selection-constraints/SameReleaseConstraint.hpp" #include "track-selection-constraints/TrackCandidateContext.hpp" #include "track-selection-constraints/TrackCandidateEvaluator.hpp" #include "track-selection-constraints/TrackMetadata.hpp" -using namespace lms; -using namespace lms::recommendation; - -namespace +namespace lms::recommendation::tests { - const db::TrackId T1{ 1 }; - const db::TrackId T2{ 2 }; - const db::TrackId T3{ 3 }; - const db::TrackId T4{ 4 }; + namespace + { + const db::TrackId T1{ 1 }; + const db::TrackId T2{ 2 }; + const db::TrackId T3{ 3 }; + const db::TrackId T4{ 4 }; - const db::ArtistId A1{ 10 }; - const db::ArtistId A2{ 20 }; + const db::ArtistId A1{ 10 }; + const db::ArtistId A2{ 20 }; - const db::ReleaseId R1{ 100 }; - const db::ReleaseId R2{ 200 }; -} // namespace + const db::ReleaseId R1{ 100 }; + const db::ReleaseId R2{ 200 }; + } // namespace -TEST(DuplicateTrackConstraint, acceptsNewCandidate) -{ - const std::vector selected{ T1, T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T3, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_FALSE(DuplicateTrackConstraint{}.rejects(ctx)); -} + TEST(DuplicateTrackConstraint, acceptsNewCandidate) + { + const std::vector selected{ T1, T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T3, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FALSE(DuplicateTrackConstraint{}.rejects(ctx)); + } -TEST(DuplicateTrackConstraint, rejectsAlreadySelected) -{ - const std::vector selected{ T1, T2, T3 }; - const TrackCandidateContext ctx{ .candidateTrackId = T2, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_TRUE(DuplicateTrackConstraint{}.rejects(ctx)); -} + TEST(DuplicateTrackConstraint, rejectsAlreadySelected) + { + const std::vector selected{ T1, T2, T3 }; + const TrackCandidateContext ctx{ .candidateTrackId = T2, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_TRUE(DuplicateTrackConstraint{}.rejects(ctx)); + } -TEST(DuplicateTrackConstraint, acceptsWhenSelectionEmpty) -{ - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; - EXPECT_FALSE(DuplicateTrackConstraint{}.rejects(ctx)); -} + TEST(DuplicateTrackConstraint, acceptsWhenSelectionEmpty) + { + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; + EXPECT_FALSE(DuplicateTrackConstraint{}.rejects(ctx)); + } -TEST(SameArtistConstraint, zeroScoreWhenNoSharedArtist) -{ - const TrackMetadataMap meta{ - { T1, { .releaseId = {}, .artistIds = { A1 } } }, - { T2, { .releaseId = {}, .artistIds = { A2 } } }, - }; - const SameArtistConstraint constraint{ meta }; - const std::vector selected{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); -} + TEST(SameArtistConstraint, zeroScoreWhenNoSharedArtist) + { + const TrackMetadataMap meta{ + { T1, { .releaseId = {}, .artistIds = { A1 } } }, + { T2, { .releaseId = {}, .artistIds = { A2 } } }, + }; + const SameArtistConstraint constraint{ meta }; + const std::vector selected{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); + } -TEST(SameArtistConstraint, fullScoreWhenMostRecentMatchesArtist) -{ - TrackMetadataMap meta{ - { T1, { .releaseId = {}, .artistIds = { A1 } } }, - { T2, { .releaseId = {}, .artistIds = { A1 } } }, - }; - const SameArtistConstraint constraint{ meta }; - const std::vector selected{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + TEST(SameArtistConstraint, fullScoreWhenMostRecentMatchesArtist) + { + TrackMetadataMap meta{ + { T1, { .releaseId = {}, .artistIds = { A1 } } }, + { T2, { .releaseId = {}, .artistIds = { A1 } } }, + }; + const SameArtistConstraint constraint{ meta }; + const std::vector selected{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 1.F); -} + EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 1.F); + } -TEST(SameArtistConstraint, halfScoreWhenSecondMostRecentMatchesArtist) -{ - TrackMetadataMap meta{ - { T1, { .releaseId = {}, .artistIds = { A1 } } }, - { T2, { .releaseId = {}, .artistIds = { A2 } } }, - { T3, { .releaseId = {}, .artistIds = { A1 } } }, - }; - const SameArtistConstraint constraint{ meta }; + TEST(SameArtistConstraint, halfScoreWhenSecondMostRecentMatchesArtist) + { + TrackMetadataMap meta{ + { T1, { .releaseId = {}, .artistIds = { A1 } } }, + { T2, { .releaseId = {}, .artistIds = { A2 } } }, + { T3, { .releaseId = {}, .artistIds = { A1 } } }, + }; + const SameArtistConstraint constraint{ meta }; - const std::vector selected{ T3, T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + const std::vector selected{ T3, T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.5F); -} + EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.5F); + } -TEST(SameArtistConstraint, trackOutsideWindowIsIgnored) -{ - TrackMetadataMap meta{ - { T1, { .releaseId = {}, .artistIds = { A1 } } }, - { T2, { .releaseId = {}, .artistIds = { A2 } } }, - { T3, { .releaseId = {}, .artistIds = { A1 } } }, - }; - const SameArtistConstraint constraint{ meta, /*window=*/1 }; + TEST(SameArtistConstraint, trackOutsideWindowIsIgnored) + { + TrackMetadataMap meta{ + { T1, { .releaseId = {}, .artistIds = { A1 } } }, + { T2, { .releaseId = {}, .artistIds = { A2 } } }, + { T3, { .releaseId = {}, .artistIds = { A1 } } }, + }; + const SameArtistConstraint constraint{ meta, /*window=*/1 }; - const std::vector selected{ T3, T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); -} + const std::vector selected{ T3, T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); + } -TEST(SameArtistConstraint, zeroScoreWhenCandidateNotInMap) -{ - const TrackMetadataMap meta{}; - const SameArtistConstraint constraint{ meta }; - const std::vector selected{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); -} + TEST(SameArtistConstraint, zeroScoreWhenCandidateNotInMap) + { + const TrackMetadataMap meta{}; + const SameArtistConstraint constraint{ meta }; + const std::vector selected{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); + } -TEST(SameReleaseConstraint, zeroScoreWhenNoSharedRelease) -{ - TrackMetadataMap meta{ - { T1, { .releaseId = R1, .artistIds = {} } }, - { T2, { .releaseId = R2, .artistIds = {} } }, - }; - const SameReleaseConstraint constraint{ meta }; - const std::vector selected{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); -} + TEST(SameReleaseConstraint, zeroScoreWhenNoSharedRelease) + { + TrackMetadataMap meta{ + { T1, { .releaseId = R1, .artistIds = {} } }, + { T2, { .releaseId = R2, .artistIds = {} } }, + }; + const SameReleaseConstraint constraint{ meta }; + const std::vector selected{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); + } -TEST(SameReleaseConstraint, fullScoreWhenMostRecentMatchesRelease) -{ - TrackMetadataMap meta{ - { T1, { .releaseId = R1, .artistIds = {} } }, - { T2, { .releaseId = R1, .artistIds = {} } }, - }; - const SameReleaseConstraint constraint{ meta }; - const std::vector selected{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 1.F); -} + TEST(SameReleaseConstraint, fullScoreWhenMostRecentMatchesRelease) + { + TrackMetadataMap meta{ + { T1, { .releaseId = R1, .artistIds = {} } }, + { T2, { .releaseId = R1, .artistIds = {} } }, + }; + const SameReleaseConstraint constraint{ meta }; + const std::vector selected{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 1.F); + } -TEST(SameReleaseConstraint, zeroScoreWhenCandidateHasNoRelease) -{ - TrackMetadataMap meta{ - { T1, { .releaseId = {}, .artistIds = {} } }, - { T2, { .releaseId = R1, .artistIds = {} } }, - }; - const SameReleaseConstraint constraint{ meta }; - const std::vector selected{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); -} + TEST(SameReleaseConstraint, zeroScoreWhenCandidateHasNoRelease) + { + TrackMetadataMap meta{ + { T1, { .releaseId = {}, .artistIds = {} } }, + { T2, { .releaseId = R1, .artistIds = {} } }, + }; + const SameReleaseConstraint constraint{ meta }; + const std::vector selected{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); + } -TEST(TrackCandidateEvaluator, hardConstraintRejects) -{ - TrackCandidateEvaluator evaluator; - evaluator.addHardConstraint(std::make_unique()); + TEST(TrackCandidateEvaluator, hardConstraintRejects) + { + TrackCandidateEvaluator evaluator; + evaluator.addHardConstraint(std::make_unique()); - const std::vector selected{ T1 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_TRUE(evaluator.rejects(ctx)); -} + const std::vector selected{ T1 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_TRUE(evaluator.rejects(ctx)); + } -TEST(TrackCandidateEvaluator, noHardConstraintDoesNotReject) -{ - TrackCandidateEvaluator evaluator; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; - EXPECT_FALSE(evaluator.rejects(ctx)); -} + TEST(TrackCandidateEvaluator, noHardConstraintDoesNotReject) + { + TrackCandidateEvaluator evaluator; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; + EXPECT_FALSE(evaluator.rejects(ctx)); + } -TEST(TrackCandidateEvaluator, softConstraintScoreIsWeighted) -{ - TrackMetadataMap meta{ - { T1, { .releaseId = R1, .artistIds = {} } }, - { T2, { .releaseId = R1, .artistIds = {} } }, - }; - TrackCandidateEvaluator evaluator; - evaluator.addSoftConstraint(std::make_unique(meta), 2.F); + TEST(TrackCandidateEvaluator, softConstraintScoreIsWeighted) + { + TrackMetadataMap meta{ + { T1, { .releaseId = R1, .artistIds = {} } }, + { T2, { .releaseId = R1, .artistIds = {} } }, + }; + TrackCandidateEvaluator evaluator; + evaluator.addSoftConstraint(std::make_unique(meta), 2.F); - const std::vector selected{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + const std::vector selected{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_FLOAT_EQ(evaluator.score(ctx), 2.F); -} + EXPECT_FLOAT_EQ(evaluator.score(ctx), 2.F); + } -TEST(TrackCandidateEvaluator, multipleSoftConstraintsAreAccumulated) -{ - TrackMetadataMap meta{ - { T1, { .releaseId = R1, .artistIds = { A1 } } }, - { T2, { .releaseId = R1, .artistIds = { A1 } } }, - }; - TrackCandidateEvaluator evaluator; - evaluator.addSoftConstraint(std::make_unique(meta), 1.F); - evaluator.addSoftConstraint(std::make_unique(meta), 1.F); + TEST(TrackCandidateEvaluator, multipleSoftConstraintsAreAccumulated) + { + TrackMetadataMap meta{ + { T1, { .releaseId = R1, .artistIds = { A1 } } }, + { T2, { .releaseId = R1, .artistIds = { A1 } } }, + }; + TrackCandidateEvaluator evaluator; + evaluator.addSoftConstraint(std::make_unique(meta), 1.F); + evaluator.addSoftConstraint(std::make_unique(meta), 1.F); - const std::vector selected{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + const std::vector selected{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_FLOAT_EQ(evaluator.score(ctx), 2.F); -} + EXPECT_FLOAT_EQ(evaluator.score(ctx), 2.F); + } -TEST(TrackCandidateEvaluator, hardConstraintPassesEvenWithSoftConstraints) -{ - TrackMetadataMap meta{ - { T1, { .releaseId = R1, .artistIds = {} } }, - { T2, { .releaseId = R1, .artistIds = {} } }, - }; - TrackCandidateEvaluator evaluator; - evaluator.addHardConstraint(std::make_unique()); - evaluator.addSoftConstraint(std::make_unique(meta), 1.F); + TEST(TrackCandidateEvaluator, hardConstraintPassesEvenWithSoftConstraints) + { + TrackMetadataMap meta{ + { T1, { .releaseId = R1, .artistIds = {} } }, + { T2, { .releaseId = R1, .artistIds = {} } }, + }; + TrackCandidateEvaluator evaluator; + evaluator.addHardConstraint(std::make_unique()); + evaluator.addSoftConstraint(std::make_unique(meta), 1.F); - const std::vector selected{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_FALSE(evaluator.rejects(ctx)); - EXPECT_FLOAT_EQ(evaluator.score(ctx), 1.F); -} + const std::vector selected{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FALSE(evaluator.rejects(ctx)); + EXPECT_FLOAT_EQ(evaluator.score(ctx), 1.F); + } -namespace -{ - using TestVec = math::Vector<2, float>; - const TestVec v_x{ 1.F, 0.F }; - const TestVec v_y{ 0.F, 1.F }; + namespace + { + using TestVec = math::Vector<2, float>; + const TestVec v_x{ 1.F, 0.F }; + const TestVec v_y{ 0.F, 1.F }; - using TestConstraint = NearDuplicateEmbeddingConstraint<2>; - using TestVectorMap = TestConstraint::TrackVectorMap; -} // namespace + using TestConstraint = NearDuplicateEmbeddingConstraint<2>; + using TestVectorMap = TestConstraint::TrackVectorMap; + } // namespace -TEST(NearDuplicateEmbeddingConstraint, acceptsWhenCandidateNotInMap) -{ - const TestVectorMap trackVectors{ { T1, &v_x } }; - const TestConstraint constraint{ trackVectors, 0.1F }; - const std::vector selected{ T1 }; - const TrackCandidateContext ctx{ .candidateTrackId = T2, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_FALSE(constraint.rejects(ctx)); -} + TEST(NearDuplicateEmbeddingConstraint, acceptsWhenCandidateNotInMap) + { + const TestVectorMap trackVectors{ { T1, &v_x } }; + const TestConstraint constraint{ trackVectors, 0.1F }; + const std::vector selected{ T1 }; + const TrackCandidateContext ctx{ .candidateTrackId = T2, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FALSE(constraint.rejects(ctx)); + } -TEST(NearDuplicateEmbeddingConstraint, acceptsWhenSelectionEmpty) -{ - const TestVectorMap trackVectors{ { T1, &v_x } }; - const TestConstraint constraint{ trackVectors, 0.1F }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; - EXPECT_FALSE(constraint.rejects(ctx)); -} + TEST(NearDuplicateEmbeddingConstraint, acceptsWhenSelectionEmpty) + { + const TestVectorMap trackVectors{ { T1, &v_x } }; + const TestConstraint constraint{ trackVectors, 0.1F }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; + EXPECT_FALSE(constraint.rejects(ctx)); + } -TEST(NearDuplicateEmbeddingConstraint, acceptsWhenSelectedTrackNotInMap) -{ - const TestVectorMap trackVectors{ { T1, &v_x } }; - const TestConstraint constraint{ trackVectors, 0.1F }; - const std::vector selected{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_FALSE(constraint.rejects(ctx)); -} + TEST(NearDuplicateEmbeddingConstraint, acceptsWhenSelectedTrackNotInMap) + { + const TestVectorMap trackVectors{ { T1, &v_x } }; + const TestConstraint constraint{ trackVectors, 0.1F }; + const std::vector selected{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FALSE(constraint.rejects(ctx)); + } -TEST(NearDuplicateEmbeddingConstraint, rejectsWhenDistanceBelowThreshold) -{ - const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } }; - const TestConstraint constraint{ trackVectors, 0.1F }; - const std::vector selected{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_TRUE(constraint.rejects(ctx)); -} + TEST(NearDuplicateEmbeddingConstraint, rejectsWhenDistanceBelowThreshold) + { + const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } }; + const TestConstraint constraint{ trackVectors, 0.1F }; + const std::vector selected{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_TRUE(constraint.rejects(ctx)); + } -TEST(NearDuplicateEmbeddingConstraint, acceptsWhenDistanceAboveThreshold) -{ - const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } }; - const TestConstraint constraint{ trackVectors, 0.1F }; - const std::vector selected{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_FALSE(constraint.rejects(ctx)); -} + TEST(NearDuplicateEmbeddingConstraint, acceptsWhenDistanceAboveThreshold) + { + const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } }; + const TestConstraint constraint{ trackVectors, 0.1F }; + const std::vector selected{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FALSE(constraint.rejects(ctx)); + } -TEST(NearDuplicateEmbeddingConstraint, rejectsWhenOneOfManySelectedIsNearDuplicate) -{ - const TestVectorMap trackVectors{ { T1, &v_x }, { T3, &v_y }, { T4, &v_x } }; - const TestConstraint constraint{ trackVectors, 0.1F }; - const std::vector selected{ T3, T4 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_TRUE(constraint.rejects(ctx)); -} + TEST(NearDuplicateEmbeddingConstraint, rejectsWhenOneOfManySelectedIsNearDuplicate) + { + const TestVectorMap trackVectors{ { T1, &v_x }, { T3, &v_y }, { T4, &v_x } }; + const TestConstraint constraint{ trackVectors, 0.1F }; + const std::vector selected{ T3, T4 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_TRUE(constraint.rejects(ctx)); + } -TEST(NearDuplicateEmbeddingConstraint, acceptsWhenAllSelectedAreFarEnough) -{ - const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y }, { T3, &v_y } }; - const TestConstraint constraint{ trackVectors, 0.1F }; - const std::vector selected{ T2, T3 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_FALSE(constraint.rejects(ctx)); -} + TEST(NearDuplicateEmbeddingConstraint, acceptsWhenAllSelectedAreFarEnough) + { + const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y }, { T3, &v_y } }; + const TestConstraint constraint{ trackVectors, 0.1F }; + const std::vector selected{ T2, T3 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FALSE(constraint.rejects(ctx)); + } -TEST(InterpolationFitConstraint, zeroScoreWhenNoSeeds) -{ - const TestVectorMap trackVectors{ { T1, &v_x } }; - const InterpolationFitConstraint<2> constraint{ trackVectors }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; - EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); -} + TEST(InterpolationFitConstraint, zeroScoreWhenNoSeeds) + { + const TestVectorMap trackVectors{ { T1, &v_x } }; + const InterpolationFitConstraint<2> constraint{ trackVectors }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; + EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); + } -TEST(InterpolationFitConstraint, zeroScoreWhenCandidateMatchesSeed) -{ - const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } }; - const InterpolationFitConstraint<2> constraint{ trackVectors }; - const std::vector seeds{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds }; - EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); -} + TEST(InterpolationFitConstraint, zeroScoreWhenCandidateMatchesSeed) + { + const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } }; + const InterpolationFitConstraint<2> constraint{ trackVectors }; + const std::vector seeds{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds }; + EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); + } -TEST(InterpolationFitConstraint, halfScoreWhenCandidateOrthogonalToSeed) -{ - const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } }; - const InterpolationFitConstraint<2> constraint{ trackVectors }; - const std::vector seeds{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds }; - EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.5F); -} + TEST(InterpolationFitConstraint, halfScoreWhenCandidateOrthogonalToSeed) + { + const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } }; + const InterpolationFitConstraint<2> constraint{ trackVectors }; + const std::vector seeds{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds }; + EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.5F); + } -TEST(InterpolationFitConstraint, usesClosestSeed) -{ - const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x }, { T3, &v_y } }; - const InterpolationFitConstraint<2> constraint{ trackVectors }; - const std::vector seeds{ T3, T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds }; - EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); -} + TEST(InterpolationFitConstraint, usesClosestSeed) + { + const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x }, { T3, &v_y } }; + const InterpolationFitConstraint<2> constraint{ trackVectors }; + const std::vector seeds{ T3, T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds }; + EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); + } -TEST(MaxDistanceConstraint, acceptsWhenNoSeeds) -{ - const TestVectorMap trackVectors{ { T1, &v_x } }; - const MaxDistanceConstraint<2> constraint{ trackVectors, 0.5F }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; - EXPECT_FALSE(constraint.rejects(ctx)); -} + TEST(MaxDistanceConstraint, acceptsWhenNoSeeds) + { + const TestVectorMap trackVectors{ { T1, &v_x } }; + const MaxDistanceConstraint<2> constraint{ trackVectors, 0.5F }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; + EXPECT_FALSE(constraint.rejects(ctx)); + } -TEST(MaxDistanceConstraint, acceptsWhenWithinThresholdOfOneSeed) -{ - const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } }; - const MaxDistanceConstraint<2> constraint{ trackVectors, 0.5F }; - const std::vector seeds{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds }; - EXPECT_FALSE(constraint.rejects(ctx)); -} + TEST(MaxDistanceConstraint, acceptsWhenWithinThresholdOfOneSeed) + { + const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } }; + const MaxDistanceConstraint<2> constraint{ trackVectors, 0.5F }; + const std::vector seeds{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds }; + EXPECT_FALSE(constraint.rejects(ctx)); + } -TEST(MaxDistanceConstraint, rejectsWhenBeyondThresholdFromAllSeeds) -{ - const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } }; - const MaxDistanceConstraint<2> constraint{ trackVectors, 0.1F }; - const std::vector seeds{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds }; - EXPECT_TRUE(constraint.rejects(ctx)); -} + TEST(MaxDistanceConstraint, rejectsWhenBeyondThresholdFromAllSeeds) + { + const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } }; + const MaxDistanceConstraint<2> constraint{ trackVectors, 0.1F }; + const std::vector seeds{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds }; + EXPECT_TRUE(constraint.rejects(ctx)); + } -TEST(MaxDistanceConstraint, acceptsWhenAnyOfManySeedsIsCloseEnough) -{ - const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y }, { T3, &v_x } }; - const MaxDistanceConstraint<2> constraint{ trackVectors, 0.5F }; - const std::vector seeds{ T2, T3 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds }; - EXPECT_FALSE(constraint.rejects(ctx)); -} + TEST(MaxDistanceConstraint, acceptsWhenAnyOfManySeedsIsCloseEnough) + { + const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y }, { T3, &v_x } }; + const MaxDistanceConstraint<2> constraint{ trackVectors, 0.5F }; + const std::vector seeds{ T2, T3 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds }; + EXPECT_FALSE(constraint.rejects(ctx)); + } -TEST(SmoothTransitionConstraint, zeroScoreWhenNoSelectedTracks) -{ - const TestVectorMap trackVectors{ { T1, &v_x } }; - const SmoothTransitionConstraint<2> constraint{ trackVectors }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; - EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); -} + TEST(SmoothTransitionConstraint, zeroScoreWhenNoSelectedTracks) + { + const TestVectorMap trackVectors{ { T1, &v_x } }; + const SmoothTransitionConstraint<2> constraint{ trackVectors }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; + EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); + } -TEST(SmoothTransitionConstraint, zeroScoreWhenPreviousMatchesCandidate) -{ - const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } }; - const SmoothTransitionConstraint<2> constraint{ trackVectors }; - const std::vector selected{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); -} + TEST(SmoothTransitionConstraint, zeroScoreWhenPreviousMatchesCandidate) + { + const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } }; + const SmoothTransitionConstraint<2> constraint{ trackVectors }; + const std::vector selected{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); + } -TEST(SmoothTransitionConstraint, halfScoreWhenPreviousOrthogonalToCandidate) -{ - const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } }; - const SmoothTransitionConstraint<2> constraint{ trackVectors }; - const std::vector selected{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.5F); -} + TEST(SmoothTransitionConstraint, halfScoreWhenPreviousOrthogonalToCandidate) + { + const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } }; + const SmoothTransitionConstraint<2> constraint{ trackVectors }; + const std::vector selected{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.5F); + } -TEST(SmoothTransitionConstraint, usesMostRecentlySelectedTrack) -{ - const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x }, { T3, &v_y } }; - const SmoothTransitionConstraint<2> constraint{ trackVectors }; - const std::vector selected{ T3, T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; - EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); -} + TEST(SmoothTransitionConstraint, usesMostRecentlySelectedTrack) + { + const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x }, { T3, &v_y } }; + const SmoothTransitionConstraint<2> constraint{ trackVectors }; + const std::vector selected{ T3, T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); + } + + namespace + { + const core::UUID MBID1{ *core::UUID::fromString("3d2508f7-3a7e-4f25-895c-fca079dd71ce") }; + const core::UUID MBID2{ *core::UUID::fromString("8e44e4ea-9bce-47c2-a48c-8aa338242fb2") }; + } // namespace + + TEST(SameRecordingMBIDConstraint, acceptsWhenCandidateNotInMap) + { + const TrackMetadataMap meta{ { T2, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } } }; + const SameRecordingMBIDConstraint constraint{ meta }; + const std::vector selected{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FALSE(constraint.rejects(ctx)); + } + + TEST(SameRecordingMBIDConstraint, acceptsWhenCandidateHasNoMBID) + { + const TrackMetadataMap meta{ + { T1, { .releaseId = {}, .artistIds = {}, .recordingMBID = std::nullopt } }, + { T2, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } }, + }; + const SameRecordingMBIDConstraint constraint{ meta }; + const std::vector selected{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FALSE(constraint.rejects(ctx)); + } + + TEST(SameRecordingMBIDConstraint, acceptsWhenSelectionEmpty) + { + const TrackMetadataMap meta{ { T1, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } } }; + const SameRecordingMBIDConstraint constraint{ meta }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; + EXPECT_FALSE(constraint.rejects(ctx)); + } + + TEST(SameRecordingMBIDConstraint, acceptsWhenNoneOfSelectedHasSameMBID) + { + const TrackMetadataMap meta{ + { T1, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } }, + { T2, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID2 } }, + }; + const SameRecordingMBIDConstraint constraint{ meta }; + const std::vector selected{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FALSE(constraint.rejects(ctx)); + } + + TEST(SameRecordingMBIDConstraint, rejectsWhenOneOfSelectedHasSameMBID) + { + const TrackMetadataMap meta{ + { T1, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } }, + { T2, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID2 } }, + { T3, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } }, + }; + const SameRecordingMBIDConstraint constraint{ meta }; + const std::vector selected{ T2, T3 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_TRUE(constraint.rejects(ctx)); + } + + TEST(SameRecordingMBIDConstraint, acceptsWhenSelectedTrackNotInMap) + { + const TrackMetadataMap meta{ { T1, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } } }; + const SameRecordingMBIDConstraint constraint{ meta }; + const std::vector selected{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FALSE(constraint.rejects(ctx)); + } + + TEST(SameRecordingMBIDConstraint, acceptsWhenSelectedTrackHasNoMBID) + { + const TrackMetadataMap meta{ + { T1, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } }, + { T2, { .releaseId = {}, .artistIds = {}, .recordingMBID = std::nullopt } }, + }; + const SameRecordingMBIDConstraint constraint{ meta }; + const std::vector selected{ T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FALSE(constraint.rejects(ctx)); + } +} // namespace lms::recommendation::tests \ No newline at end of file