From 028e710f02710c56338b3d986671ebfdcabc095d Mon Sep 17 00:00:00 2001 From: Jean-Pierre Briede Date: Mon, 12 Aug 2024 10:34:28 -0700 Subject: [PATCH 1/6] WIP: Refactoring to encapsulate Recommender logic --- .../PackageFeeds/CombinedPackageFeed.cs | 60 +++++++++++++++ .../InstalledAndTransitivePackageFeed.cs | 2 +- .../PackageFeeds/InstalledPackageFeed.cs | 4 +- .../PackageFeeds/RecommenderPackageFeed.cs | 6 +- .../Services/NuGetPackageSearchService.cs | 57 +++++++------- .../Services/SearchObject.cs | 74 ++++--------------- .../PackageSearchMetadataContextInfo.cs | 12 +-- .../RecommendedPackageSearchMetadata.cs | 55 ++++++++++++++ .../PackageItemLoaderTests.cs | 9 ++- .../NuGetPackageSearchServiceTests.cs | 8 +- 10 files changed, 178 insertions(+), 109 deletions(-) create mode 100644 src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/CombinedPackageFeed.cs create mode 100644 src/NuGet.Clients/NuGet.VisualStudio.Internal.Contracts/RecommendedPackageSearchMetadata.cs diff --git a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/CombinedPackageFeed.cs b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/CombinedPackageFeed.cs new file mode 100644 index 00000000000..437963d0322 --- /dev/null +++ b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/CombinedPackageFeed.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NuGet.Protocol.Core.Types; + +namespace NuGet.PackageManagement.VisualStudio.PackageFeeds +{ + internal class CombinedPackageFeed : IPackageFeed + { + private readonly IPackageFeed _mainFeed; + private readonly IPackageFeed _recommenderFeed; + + public CombinedPackageFeed(IPackageFeed mainFeed, IPackageFeed recommenderFeed) + { + _mainFeed = mainFeed ?? throw new ArgumentNullException(nameof(mainFeed)); + _recommenderFeed = recommenderFeed ?? throw new ArgumentNullException(nameof(recommenderFeed)); + } + + public bool IsMultiSource => _mainFeed.IsMultiSource; + + public Task> ContinueSearchAsync(ContinuationToken continuationToken, CancellationToken cancellationToken) + { + return _mainFeed.ContinueSearchAsync(continuationToken, cancellationToken); + } + + public Task> RefreshSearchAsync(RefreshToken refreshToken, CancellationToken cancellationToken) + { + return _mainFeed.RefreshSearchAsync(refreshToken, cancellationToken); + } + + public async Task> SearchAsync(string searchText, SearchFilter filter, CancellationToken cancellationToken) + { + SearchResult mainFeedResults = await _mainFeed.SearchAsync(searchText, filter, cancellationToken); + SearchResult recommenderResults = await _recommenderFeed.SearchAsync(searchText, filter, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + + var equalityComparer = new IdentityIdEqualityComparer(); + IReadOnlyList combinedResults = recommenderResults.Items.Union(mainFeedResults.Items, equalityComparer).ToList(); + return SearchResult.FromItems(combinedResults); + } + + private class IdentityIdEqualityComparer : IEqualityComparer + { + public bool Equals(IPackageSearchMetadata x, IPackageSearchMetadata y) + { + return x.Identity.Id.Equals(y.Identity.Id, StringComparison.OrdinalIgnoreCase); + } + + public int GetHashCode(IPackageSearchMetadata obj) + { + return obj.Identity.Id.GetHashCode(); + } + } + } +} diff --git a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/InstalledAndTransitivePackageFeed.cs b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/InstalledAndTransitivePackageFeed.cs index 4b168b91d49..619aa09a8a7 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/InstalledAndTransitivePackageFeed.cs +++ b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/InstalledAndTransitivePackageFeed.cs @@ -107,7 +107,7 @@ private static IPackageSearchMetadata GetMetadataFromIdentityForPackage(T ide return PackageSearchMetadataBuilder.FromIdentity(identity).Build(); } - internal override async Task GetPackageMetadataAsync(T identity, bool includePrerelease, CancellationToken cancellationToken) + internal override async Task GetPackageMetadataAsync(PackageIdentity identity, bool includePrerelease, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/InstalledPackageFeed.cs b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/InstalledPackageFeed.cs index d0ac4f2f44f..f950947e42a 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/InstalledPackageFeed.cs +++ b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/InstalledPackageFeed.cs @@ -47,7 +47,7 @@ public override async Task> ContinueSearchA return CreateResult(searchItems); } - internal async Task GetMetadataForPackagesAsync(T[] packages, bool includePrerelease, CancellationToken cancellationToken) where T : PackageIdentity + internal async Task GetMetadataForPackagesAsync(PackageIdentity[] packages, bool includePrerelease, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -88,7 +88,7 @@ internal static SearchResult CreateResult(IPackageSearch return result; } - internal virtual async Task GetPackageMetadataAsync(T identity, bool includePrerelease, CancellationToken cancellationToken) where T : PackageIdentity + internal virtual async Task GetPackageMetadataAsync(PackageIdentity identity, bool includePrerelease, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/RecommenderPackageFeed.cs b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/RecommenderPackageFeed.cs index 5ef8f2159f4..ac35ddeafd6 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/RecommenderPackageFeed.cs +++ b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/RecommenderPackageFeed.cs @@ -97,7 +97,7 @@ public Task> SearchAsync(string searchText, return RecommendPackagesAsync(searchToken, cancellationToken); } - public async Task> RecommendPackagesAsync(ContinuationToken continuationToken, CancellationToken cancellationToken) + private async Task> RecommendPackagesAsync(ContinuationToken continuationToken, CancellationToken cancellationToken) { var searchToken = continuationToken as RecommendSearchToken; if (searchToken is null) @@ -184,7 +184,7 @@ public Task> ContinueSearchAsync(Continuati public Task> RefreshSearchAsync(RefreshToken refreshToken, CancellationToken cancellationToken) => System.Threading.Tasks.Task.FromResult(SearchResult.Empty()); - public async Task GetPackageMetadataAsync(PackageIdentity identity, bool includePrerelease, CancellationToken cancellationToken) + private async Task GetPackageMetadataAsync(PackageIdentity identity, bool includePrerelease, CancellationToken cancellationToken) { // first we try and load the metadata from a local package var packageMetadata = await _metadataProvider.GetLocalPackageMetadataAsync(identity, includePrerelease, cancellationToken); @@ -193,7 +193,7 @@ public async Task GetPackageMetadataAsync(PackageIdentit // and failing that we go to the network packageMetadata = await _metadataProvider.GetPackageMetadataAsync(identity, includePrerelease, cancellationToken); } - return packageMetadata; + return new RecommendedPackageSearchMetadata(packageMetadata, VersionInfo); } } diff --git a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/Services/NuGetPackageSearchService.cs b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/Services/NuGetPackageSearchService.cs index a6b59e8b477..e0fa7ebcc6c 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/Services/NuGetPackageSearchService.cs +++ b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/Services/NuGetPackageSearchService.cs @@ -16,6 +16,7 @@ using Microsoft.ServiceHub.Framework.Services; using NuGet.Common; using NuGet.Configuration; +using NuGet.PackageManagement.VisualStudio.PackageFeeds; using NuGet.Packaging; using NuGet.Packaging.Core; using NuGet.Protocol; @@ -80,7 +81,7 @@ public async ValueTask> Ge bool recommendPackages = false; IReadOnlyCollection sourceRepositories = await _sharedServiceState.GetRepositoriesAsync(packageSources, cancellationToken); - (IPackageFeed? mainFeed, IPackageFeed? recommenderFeed) packageFeeds = await CreatePackageFeedAsync( + IPackageFeed packageFeed = await CreatePackageFeedAsync( projectContextInfos, targetFrameworks, itemFilter, @@ -88,8 +89,7 @@ public async ValueTask> Ge recommendPackages, sourceRepositories, cancellationToken); - - Assumes.NotNull(packageFeeds.mainFeed); + Assumes.NotNull(packageFeed); SourceRepository packagesFolderSourceRepository = await _packagesFolderLocalRepositoryLazy.GetValueAsync(cancellationToken); IEnumerable globalPackageFolderRepositories = await GetAllPackageFoldersAsync(projectContextInfos, cancellationToken); @@ -99,7 +99,7 @@ public async ValueTask> Ge globalPackageFolderRepositories, new VisualStudioActivityLogger()); - var searchObject = new SearchObject(packageFeeds.mainFeed, packageFeeds.recommenderFeed, metadataProvider, packageSources, PackageSearchMetadataMemoryCache); + var searchObject = new SearchObject(packageFeed, metadataProvider, packageSources, PackageSearchMetadataMemoryCache); return await searchObject.GetAllPackagesAsync(searchFilter, cancellationToken); } @@ -264,7 +264,7 @@ public async ValueTask SearchAsync( Assumes.NotNull(searchFilter); IReadOnlyCollection? sourceRepositories = await _sharedServiceState.GetRepositoriesAsync(packageSources, cancellationToken); - (IPackageFeed? mainFeed, IPackageFeed? recommenderFeed) = await CreatePackageFeedAsync( + IPackageFeed packageFeed = await CreatePackageFeedAsync( projectContextInfos, targetFrameworks, itemFilter, @@ -272,7 +272,7 @@ public async ValueTask SearchAsync( useRecommender, sourceRepositories, cancellationToken); - Assumes.NotNull(mainFeed); + Assumes.NotNull(packageFeed); SourceRepository packagesFolderSourceRepository = await _packagesFolderLocalRepositoryLazy.GetValueAsync(cancellationToken); IEnumerable globalPackageFolderRepositories = await GetAllPackageFoldersAsync(projectContextInfos, cancellationToken); @@ -282,7 +282,7 @@ public async ValueTask SearchAsync( globalPackageFolderRepositories, new VisualStudioActivityLogger()); - _searchObject = new SearchObject(mainFeed, recommenderFeed, metadataProvider, packageSources, PackageSearchMetadataMemoryCache); + _searchObject = new SearchObject(packageFeed, metadataProvider, packageSources, PackageSearchMetadataMemoryCache); return await _searchObject.SearchAsync(searchText, searchFilter, useRecommender, cancellationToken); } @@ -301,8 +301,8 @@ public async ValueTask GetTotalCountAsync( Assumes.NotNull(searchFilter); IReadOnlyCollection? sourceRepositories = await _sharedServiceState.GetRepositoriesAsync(packageSources, cancellationToken); - (IPackageFeed? mainFeed, IPackageFeed? recommenderFeed) = await CreatePackageFeedAsync(projectContextInfos, targetFrameworks, itemFilter, isSolution, recommendPackages: false, sourceRepositories, cancellationToken); - Assumes.NotNull(mainFeed); + IPackageFeed packageFeed = await CreatePackageFeedAsync(projectContextInfos, targetFrameworks, itemFilter, isSolution, recommendPackages: false, sourceRepositories, cancellationToken); + Assumes.NotNull(packageFeed); SourceRepository packagesFolderSourceRepository = await _packagesFolderLocalRepositoryLazy.GetValueAsync(cancellationToken); IEnumerable globalPackageFolderRepositories = await GetAllPackageFoldersAsync(projectContextInfos, cancellationToken); @@ -312,7 +312,7 @@ public async ValueTask GetTotalCountAsync( globalPackageFolderRepositories, new VisualStudioActivityLogger()); - var searchObject = new SearchObject(mainFeed, recommenderFeed, metadataProvider, packageSources, searchCache: null); + var searchObject = new SearchObject(packageFeed, metadataProvider, packageSources, searchCache: null); return await searchObject.GetTotalCountAsync(maxCount, searchFilter, cancellationToken); } @@ -383,7 +383,7 @@ public async Task> GetAllPackageFoldersAsync( return allLocalFolders; } - internal async Task<(IPackageFeed? mainFeed, IPackageFeed? recommenderFeed)> CreatePackageFeedAsync( + internal async Task CreatePackageFeedAsync( IReadOnlyCollection projectContextInfos, IReadOnlyCollection targetFrameworks, ItemFilter itemFilter, @@ -394,12 +394,12 @@ public async Task> GetAllPackageFoldersAsync( { var logger = new VisualStudioActivityLogger(); var uiLogger = await ServiceLocator.GetComponentModelServiceAsync(); - var packageFeeds = (mainFeed: (IPackageFeed?)null, recommenderFeed: (IPackageFeed?)null); + IPackageFeed? packageFeed = null; if (itemFilter == ItemFilter.All && recommendPackages == false) { - packageFeeds.mainFeed = new MultiSourcePackageFeed(sourceRepositories, uiLogger, TelemetryActivity.NuGetTelemetryService); - return packageFeeds; + packageFeed = new MultiSourcePackageFeed(sourceRepositories, uiLogger, TelemetryActivity.NuGetTelemetryService); + return packageFeed; } IEnumerable globalPackageFolderRepositories = await GetAllPackageFoldersAsync(projectContextInfos, cancellationToken); @@ -418,24 +418,25 @@ public async Task> GetAllPackageFoldersAsync( PackageCollection transitivePackageCollection = PackageCollection.FromPackageReferences(browseTabPackages.TransitivePackages); // if we get here, recommendPackages == true - packageFeeds.mainFeed = new MultiSourcePackageFeed(sourceRepositories, uiLogger, TelemetryActivity.NuGetTelemetryService); + packageFeed = new MultiSourcePackageFeed(sourceRepositories, uiLogger, TelemetryActivity.NuGetTelemetryService); try { // Recommender needs installed and transitive package lists, but it does not need transitive origins data. - packageFeeds.recommenderFeed = new RecommenderPackageFeed( + IPackageFeed recommenderFeed = new RecommenderPackageFeed( sourceRepositories, installedPackageCollection, transitivePackageCollection, targetFrameworks, metadataProvider, logger); + return new CombinedPackageFeed(packageFeed, recommenderFeed); } catch (System.IO.FileNotFoundException) { // This could happen if the user disables the recommender extension. Catching this // exception allows the package manager to continue without recommendations. + return packageFeed; } - return packageFeeds; } if (itemFilter == ItemFilter.Installed) @@ -445,9 +446,7 @@ public async Task> GetAllPackageFoldersAsync( PackageCollection installedPackageCollection = PackageCollection.FromPackageReferences(installedTabWithTransitiveOrigins.InstalledPackages); PackageCollection transitivePackageCollection = PackageCollection.FromPackageReferences(installedTabWithTransitiveOrigins.TransitivePackages); - packageFeeds.mainFeed = new InstalledAndTransitivePackageFeed(installedPackageCollection, transitivePackageCollection, metadataProvider); - - return packageFeeds; + return new InstalledAndTransitivePackageFeed(installedPackageCollection, transitivePackageCollection, metadataProvider); } if (itemFilter == ItemFilter.Consolidate) @@ -456,15 +455,15 @@ public async Task> GetAllPackageFoldersAsync( IReadOnlyCollection installedTabPackages = await GetAllInstalledPackagesAsync(projectContextInfos, cancellationToken); PackageCollection installedPackageCollection = PackageCollection.FromPackageReferences(installedTabPackages); - packageFeeds.mainFeed = new ConsolidatePackageFeed(installedPackageCollection, metadataProvider, logger); - return packageFeeds; + packageFeed = new ConsolidatePackageFeed(installedPackageCollection, metadataProvider, logger); + return packageFeed; } - // Search all / updates available cannot work without a source repo - if (sourceRepositories == null) - { - return packageFeeds; - } + //// Search all / updates available cannot work without a source repo + //if (sourceRepositories == null) + //{ + // return packageFeed; + //} if (itemFilter == ItemFilter.UpdatesAvailable) { @@ -472,13 +471,13 @@ public async Task> GetAllPackageFoldersAsync( IReadOnlyCollection updatedTabPackages = await GetAllInstalledPackagesAsync(projectContextInfos, cancellationToken); PackageCollection installedPackageCollection = PackageCollection.FromPackageReferences(updatedTabPackages); - packageFeeds.mainFeed = new UpdatePackageFeed( + packageFeed = new UpdatePackageFeed( _serviceBroker, installedPackageCollection, metadataProvider, projectContextInfos.ToArray()); - return packageFeeds; + return packageFeed; } throw new InvalidOperationException( diff --git a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/Services/SearchObject.cs b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/Services/SearchObject.cs index ab6ea010d18..95e64a67f14 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/Services/SearchObject.cs +++ b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/Services/SearchObject.cs @@ -23,8 +23,7 @@ namespace NuGet.PackageManagement.VisualStudio { internal sealed class SearchObject { - private readonly IPackageFeed _mainFeed; - private readonly IPackageFeed? _recommenderFeed; + private readonly IPackageFeed _packageFeed; private SearchResult? _lastMainFeedSearchResult; private SearchFilter? _lastSearchFilter; private readonly IReadOnlyCollection _packageSources; @@ -40,7 +39,6 @@ internal sealed class SearchObject public SearchObject( IPackageFeed mainFeed, - IPackageFeed? recommenderFeed, IPackageMetadataProvider packageMetadataProvider, IReadOnlyCollection packageSources, MemoryCache? searchCache) @@ -49,8 +47,7 @@ public SearchObject( Assumes.NotNull(packageMetadataProvider); Assumes.NotNullOrEmpty(packageSources); - _mainFeed = mainFeed; - _recommenderFeed = recommenderFeed; + _packageFeed = mainFeed; _packageSources = packageSources; _packageMetadataProvider = packageMetadataProvider; _ownerDetailsUriService = _packageMetadataProvider as IOwnerDetailsUriService; @@ -59,57 +56,14 @@ public SearchObject( public async ValueTask SearchAsync(string searchText, SearchFilter filter, bool useRecommender, CancellationToken cancellationToken) { - SearchResult? mainFeedResult = await _mainFeed.SearchAsync(searchText, filter, cancellationToken); - SearchResult? recommenderFeedResults = null; - - if (useRecommender && _recommenderFeed != null) - { - recommenderFeedResults = await _recommenderFeed.SearchAsync(searchText, filter, cancellationToken); - } - + SearchResult? feedResults = await _packageFeed.SearchAsync(searchText, filter, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); - _lastMainFeedSearchResult = mainFeedResult; // Store this so we can ContinueSearch, we don't store recommended as we only do that on the first search + _lastMainFeedSearchResult = feedResults; // Store this so we can ContinueSearch, we don't store recommended as we only do that on the first search _lastSearchFilter = filter; - if (recommenderFeedResults != null) - { - // remove duplicated recommended packages from the browse results - List recommendedIds = recommenderFeedResults.Items.Select(item => item.Identity.Id).ToList(); - var recommendedPackageSearchMetadataContextInfo = new List(); - - foreach (IPackageSearchMetadata recommendedFeedResultItem in recommenderFeedResults.Items) - { - CacheBackgroundData(recommendedFeedResultItem, filter.IncludePrerelease); - var knownOwners = CreateKnownOwners(recommendedFeedResultItem); - recommendedPackageSearchMetadataContextInfo.Add( - PackageSearchMetadataContextInfo.Create( - recommendedFeedResultItem, - isRecommended: true, - recommenderVersion: (_recommenderFeed as RecommenderPackageFeed)?.VersionInfo, - knownOwners - )); - } - - foreach (IPackageSearchMetadata mainFeedResultItem in mainFeedResult.Items) - { - if (!recommendedIds.Contains(mainFeedResultItem.Identity.Id)) - { - CacheBackgroundData(mainFeedResultItem, filter.IncludePrerelease); - var knownOwners = CreateKnownOwners(mainFeedResultItem); - recommendedPackageSearchMetadataContextInfo.Add(PackageSearchMetadataContextInfo.Create(mainFeedResultItem, knownOwners)); - } - } - - return new SearchResultContextInfo( - recommendedPackageSearchMetadataContextInfo.ToList(), - mainFeedResult.SourceSearchStatus.ToImmutableDictionary(), - mainFeedResult.NextToken != null, - mainFeedResult.OperationId); - } - - var packageSearchMetadataContextInfoCollection = new List(mainFeedResult.Items.Count); - foreach (IPackageSearchMetadata packageSearchMetadata in mainFeedResult.Items) + var packageSearchMetadataContextInfoCollection = new List(feedResults.Items.Count); + foreach (IPackageSearchMetadata packageSearchMetadata in feedResults.Items) { IPackageSearchMetadata? localPackageSearchMetadata = null; @@ -123,9 +77,9 @@ public async ValueTask SearchAsync(string searchText, S return new SearchResultContextInfo( packageSearchMetadataContextInfoCollection, - mainFeedResult.SourceSearchStatus.ToImmutableDictionary(), - mainFeedResult.NextToken != null, - mainFeedResult.OperationId); + feedResults.SourceSearchStatus.ToImmutableDictionary(), + feedResults.NextToken != null, + feedResults.OperationId); } public async ValueTask RefreshSearchAsync(CancellationToken cancellationToken) @@ -133,7 +87,7 @@ public async ValueTask RefreshSearchAsync(CancellationT Assumes.NotNull(_lastMainFeedSearchResult); Assumes.NotNull(_lastSearchFilter); - SearchResult refreshSearchResult = await _mainFeed.RefreshSearchAsync( + SearchResult refreshSearchResult = await _packageFeed.RefreshSearchAsync( _lastMainFeedSearchResult.RefreshToken, cancellationToken); _lastMainFeedSearchResult = refreshSearchResult; @@ -183,7 +137,7 @@ public async ValueTask ContinueSearchAsync(Cancellation return new SearchResultContextInfo(_lastMainFeedSearchResult.OperationId); } - SearchResult continueSearchResult = await _mainFeed.ContinueSearchAsync( + SearchResult continueSearchResult = await _packageFeed.ContinueSearchAsync( _lastMainFeedSearchResult.NextToken, cancellationToken); _lastMainFeedSearchResult = continueSearchResult; @@ -216,12 +170,12 @@ public async ValueTask GetTotalCountAsync(int maxCount, SearchFilter filter do { SearchResult searchResult = nextToken == null - ? await _mainFeed.SearchAsync(string.Empty, filter, cancellationToken) - : await _mainFeed.ContinueSearchAsync(nextToken, cancellationToken); + ? await _packageFeed.SearchAsync(string.Empty, filter, cancellationToken) + : await _packageFeed.ContinueSearchAsync(nextToken, cancellationToken); while (searchResult.RefreshToken != null) { - searchResult = await _mainFeed.RefreshSearchAsync(searchResult.RefreshToken, cancellationToken); + searchResult = await _packageFeed.RefreshSearchAsync(searchResult.RefreshToken, cancellationToken); } totalCount += searchResult.Items?.Count() ?? 0; nextToken = searchResult.NextToken; diff --git a/src/NuGet.Clients/NuGet.VisualStudio.Internal.Contracts/ContextInfos/PackageSearchMetadataContextInfo.cs b/src/NuGet.Clients/NuGet.VisualStudio.Internal.Contracts/ContextInfos/PackageSearchMetadataContextInfo.cs index 9ec770cbd70..58ef8993eb9 100644 --- a/src/NuGet.Clients/NuGet.VisualStudio.Internal.Contracts/ContextInfos/PackageSearchMetadataContextInfo.cs +++ b/src/NuGet.Clients/NuGet.VisualStudio.Internal.Contracts/ContextInfos/PackageSearchMetadataContextInfo.cs @@ -44,16 +44,12 @@ public sealed class PackageSearchMetadataContextInfo public static PackageSearchMetadataContextInfo Create(IPackageSearchMetadata packageSearchMetadata) { - return Create(packageSearchMetadata, isRecommended: false, recommenderVersion: null, knownOwners: null); + return Create(packageSearchMetadata, knownOwners: null); } public static PackageSearchMetadataContextInfo Create(IPackageSearchMetadata packageSearchMetadata, IReadOnlyList? knownOwners) { - return Create(packageSearchMetadata, isRecommended: false, recommenderVersion: null, knownOwners); - } - - public static PackageSearchMetadataContextInfo Create(IPackageSearchMetadata packageSearchMetadata, bool isRecommended, (string, string)? recommenderVersion, IReadOnlyList? knownOwners) - { + var recommendedPackageSearchMetadata = packageSearchMetadata as RecommendedPackageSearchMetadata; return new PackageSearchMetadataContextInfo() { Title = packageSearchMetadata.Title, @@ -65,8 +61,8 @@ public static PackageSearchMetadataContextInfo Create(IPackageSearchMetadata pac LicenseUrl = packageSearchMetadata.LicenseUrl, ReadmeUrl = packageSearchMetadata.ReadmeUrl, LicenseMetadata = packageSearchMetadata.LicenseMetadata, - IsRecommended = isRecommended, - RecommenderVersion = recommenderVersion, + IsRecommended = recommendedPackageSearchMetadata?.IsRecommended ?? false, + RecommenderVersion = recommendedPackageSearchMetadata?.RecommenderVersion, ProjectUrl = packageSearchMetadata.ProjectUrl, Published = packageSearchMetadata.Published, OwnersList = packageSearchMetadata.OwnersList, diff --git a/src/NuGet.Clients/NuGet.VisualStudio.Internal.Contracts/RecommendedPackageSearchMetadata.cs b/src/NuGet.Clients/NuGet.VisualStudio.Internal.Contracts/RecommendedPackageSearchMetadata.cs new file mode 100644 index 00000000000..d5a2796a6af --- /dev/null +++ b/src/NuGet.Clients/NuGet.VisualStudio.Internal.Contracts/RecommendedPackageSearchMetadata.cs @@ -0,0 +1,55 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using NuGet.Packaging; +using NuGet.Packaging.Core; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; + +namespace NuGet.VisualStudio.Internal.Contracts +{ + public class RecommendedPackageSearchMetadata : IPackageSearchMetadata + { + private readonly IPackageSearchMetadata _inner; + public bool IsRecommended { get; } + public (string, string)? RecommenderVersion { get; } + + public RecommendedPackageSearchMetadata(IPackageSearchMetadata inner, (string, string)? recommenderVersion) + { + _inner = inner ?? throw new ArgumentNullException(nameof(inner)); + IsRecommended = true; + RecommenderVersion = recommenderVersion; + } + + // Implement IPackageSearchMetadata by delegating to the inner instance + public string Authors => _inner.Authors; + public IEnumerable DependencySets => _inner.DependencySets; + public string Description => _inner.Description; + public long? DownloadCount => _inner.DownloadCount; + public Uri IconUrl => _inner.IconUrl; + public PackageIdentity Identity => _inner.Identity; + public Uri LicenseUrl => _inner.LicenseUrl; + public Uri ProjectUrl => _inner.ProjectUrl; + public Uri ReadmeUrl => _inner.ReadmeUrl; + public Uri ReportAbuseUrl => _inner.ReportAbuseUrl; + public Uri PackageDetailsUrl => _inner.PackageDetailsUrl; + public DateTimeOffset? Published => _inner.Published; + public IReadOnlyList OwnersList => _inner.OwnersList; + public string Owners => _inner.Owners; + public bool RequireLicenseAcceptance => _inner.RequireLicenseAcceptance; + public string Summary => _inner.Summary; + public string Tags => _inner.Tags; + public string Title => _inner.Title; + public bool IsListed => _inner.IsListed; + public bool PrefixReserved => _inner.PrefixReserved; + public LicenseMetadata LicenseMetadata => _inner.LicenseMetadata; + public Task GetDeprecationMetadataAsync() => _inner.GetDeprecationMetadataAsync(); + + public Task> GetVersionsAsync() => _inner.GetVersionsAsync(); + + public IEnumerable Vulnerabilities => _inner.Vulnerabilities; + } +} diff --git a/test/NuGet.Clients.Tests/NuGet.PackageManagement.UI.Test/PackageItemLoaderTests.cs b/test/NuGet.Clients.Tests/NuGet.PackageManagement.UI.Test/PackageItemLoaderTests.cs index 0426242ab3f..f482c99370b 100644 --- a/test/NuGet.Clients.Tests/NuGet.PackageManagement.UI.Test/PackageItemLoaderTests.cs +++ b/test/NuGet.Clients.Tests/NuGet.PackageManagement.UI.Test/PackageItemLoaderTests.cs @@ -404,11 +404,14 @@ public async Task GetCurrent_HasKnownOwners_IsRecommendedPackage_DoesNotCreateKn { var versionString = "4.3.0"; var version = NuGetVersion.Parse(versionString); - var packageSearchMetadata = new PackageSearchMetadataBuilder.ClonedPackageSearchMetadata() + var recommendedPackageSearchMetadata = new RecommendedPackageSearchMetadata( + new PackageSearchMetadataBuilder.ClonedPackageSearchMetadata() { Identity = new PackageIdentity("NuGet.Versioning", version), OwnersList = new List { "owner1", "owner2" }, - }; + }, + recommenderVersion: (versionString, versionString)); + PackageSource packageSource = new PackageSource("https://nuget.test/v3/index.json"); Mock ownerDetailsUriService = new Mock(); ownerDetailsUriService.Setup(x => x.SupportsKnownOwners).Returns(true); @@ -422,7 +425,7 @@ public async Task GetCurrent_HasKnownOwners_IsRecommendedPackage_DoesNotCreateKn knownOwner2 }; - var packageSearchMetadataContextInfo = PackageSearchMetadataContextInfo.Create(packageSearchMetadata, isRecommended: true, recommenderVersion: (versionString, versionString), knownOwners); + var packageSearchMetadataContextInfo = PackageSearchMetadataContextInfo.Create(recommendedPackageSearchMetadata, knownOwners); var searchResult = new SearchResultContextInfo(new[] { packageSearchMetadataContextInfo }, new Dictionary { { "Search", LoadingStatus.Loading } }, hasMoreItems: false); var serviceBroker = Mock.Of(); var testVersions = new List() { diff --git a/test/NuGet.Clients.Tests/NuGet.PackageManagement.VisualStudio.Test/Services/NuGetPackageSearchServiceTests.cs b/test/NuGet.Clients.Tests/NuGet.PackageManagement.VisualStudio.Test/Services/NuGetPackageSearchServiceTests.cs index 2000f615c24..9e012d6d21a 100644 --- a/test/NuGet.Clients.Tests/NuGet.PackageManagement.VisualStudio.Test/Services/NuGetPackageSearchServiceTests.cs +++ b/test/NuGet.Clients.Tests/NuGet.PackageManagement.VisualStudio.Test/Services/NuGetPackageSearchServiceTests.cs @@ -11,6 +11,7 @@ using Microsoft.ServiceHub.Framework; using Microsoft.ServiceHub.Framework.Services; using Microsoft.VisualStudio.ComponentModelHost; +using Microsoft.VisualStudio.PlatformUI; using Microsoft.VisualStudio.Sdk.TestFramework; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -21,6 +22,7 @@ using NuGet.Commands; using NuGet.Common; using NuGet.Configuration; +using NuGet.PackageManagement.VisualStudio.PackageFeeds; using NuGet.Packaging.Core; using NuGet.ProjectManagement; using NuGet.Protocol; @@ -391,7 +393,7 @@ public async Task CreatePackageFeedAsync_WithTransitiveOrigins_OnlyInstalledFeed using NuGetPackageSearchService searchService = SetupSearchService(); // Act - (IPackageFeed main, IPackageFeed recommender) = await searchService.CreatePackageFeedAsync( + IPackageFeed packageFeed = await searchService.CreatePackageFeedAsync( projectContextInfos: _projects, targetFrameworks: new List() { "net45" }, itemFilter: itemFilter, @@ -401,8 +403,8 @@ public async Task CreatePackageFeedAsync_WithTransitiveOrigins_OnlyInstalledFeed cancellationToken: CancellationToken.None); // Assert - Assert.IsType(expectedFeedType, main); - Assert.Null(recommender); + Assert.IsType(expectedFeedType, packageFeed); + Assert.IsNotType(packageFeed); } private NuGetPackageSearchService SetupSearchService() From 2fd2ad5db5fcb34ba8a21ba65e61aec5bf10ec32 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Briede Date: Thu, 3 Oct 2024 19:51:02 -0700 Subject: [PATCH 2/6] Refactor the Recommender feed to decorate another feed and add the results to the top --- .../PackageFeeds/CombinedPackageFeed.cs | 60 ------------------- .../PackageFeeds/RecommenderPackageFeed.cs | 37 ++++++++++-- .../Services/NuGetPackageSearchService.cs | 5 +- 3 files changed, 34 insertions(+), 68 deletions(-) delete mode 100644 src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/CombinedPackageFeed.cs diff --git a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/CombinedPackageFeed.cs b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/CombinedPackageFeed.cs deleted file mode 100644 index 437963d0322..00000000000 --- a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/CombinedPackageFeed.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using NuGet.Protocol.Core.Types; - -namespace NuGet.PackageManagement.VisualStudio.PackageFeeds -{ - internal class CombinedPackageFeed : IPackageFeed - { - private readonly IPackageFeed _mainFeed; - private readonly IPackageFeed _recommenderFeed; - - public CombinedPackageFeed(IPackageFeed mainFeed, IPackageFeed recommenderFeed) - { - _mainFeed = mainFeed ?? throw new ArgumentNullException(nameof(mainFeed)); - _recommenderFeed = recommenderFeed ?? throw new ArgumentNullException(nameof(recommenderFeed)); - } - - public bool IsMultiSource => _mainFeed.IsMultiSource; - - public Task> ContinueSearchAsync(ContinuationToken continuationToken, CancellationToken cancellationToken) - { - return _mainFeed.ContinueSearchAsync(continuationToken, cancellationToken); - } - - public Task> RefreshSearchAsync(RefreshToken refreshToken, CancellationToken cancellationToken) - { - return _mainFeed.RefreshSearchAsync(refreshToken, cancellationToken); - } - - public async Task> SearchAsync(string searchText, SearchFilter filter, CancellationToken cancellationToken) - { - SearchResult mainFeedResults = await _mainFeed.SearchAsync(searchText, filter, cancellationToken); - SearchResult recommenderResults = await _recommenderFeed.SearchAsync(searchText, filter, cancellationToken); - cancellationToken.ThrowIfCancellationRequested(); - - var equalityComparer = new IdentityIdEqualityComparer(); - IReadOnlyList combinedResults = recommenderResults.Items.Union(mainFeedResults.Items, equalityComparer).ToList(); - return SearchResult.FromItems(combinedResults); - } - - private class IdentityIdEqualityComparer : IEqualityComparer - { - public bool Equals(IPackageSearchMetadata x, IPackageSearchMetadata y) - { - return x.Identity.Id.Equals(y.Identity.Id, StringComparison.OrdinalIgnoreCase); - } - - public int GetHashCode(IPackageSearchMetadata obj) - { - return obj.Identity.Id.GetHashCode(); - } - } - } -} diff --git a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/RecommenderPackageFeed.cs b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/RecommenderPackageFeed.cs index ac35ddeafd6..3c15abbd273 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/RecommenderPackageFeed.cs +++ b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/RecommenderPackageFeed.cs @@ -19,11 +19,13 @@ namespace NuGet.PackageManagement.VisualStudio { /// /// Represents a package feed which recommends packages based on currently loaded project info + /// Decorates any other feed and adds recommendations to the top of the list /// public class RecommenderPackageFeed : IPackageFeed { - public bool IsMultiSource => false; + public bool IsMultiSource => _decoratedPackageFeed.IsMultiSource; + private readonly IPackageFeed _decoratedPackageFeed; private readonly SourceRepository _sourceRepository; private readonly List _installedPackages; private readonly List _transitivePackages; @@ -40,6 +42,7 @@ public class RecommenderPackageFeed : IPackageFeed private const int MaxRecommended = 5; public RecommenderPackageFeed( + IPackageFeed decoratedFeed, IEnumerable sourceRepositories, PackageCollection installedPackages, PackageCollection transitivePackages, @@ -59,6 +62,7 @@ public RecommenderPackageFeed( { throw new ArgumentNullException(nameof(transitivePackages)); } + _decoratedPackageFeed = decoratedFeed ?? throw new ArgumentNullException(nameof(decoratedFeed)); _targetFrameworks = targetFrameworks ?? throw new ArgumentNullException(nameof(targetFrameworks)); _metadataProvider = metadataProvider ?? throw new ArgumentNullException(nameof(metadataProvider)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -86,7 +90,7 @@ private class RecommendSearchToken : ContinuationToken public SearchFilter SearchFilter { get; set; } } - public Task> SearchAsync(string searchText, SearchFilter searchFilter, CancellationToken cancellationToken) + public async Task> SearchAsync(string searchText, SearchFilter searchFilter, CancellationToken cancellationToken) { var searchToken = new RecommendSearchToken { @@ -94,7 +98,16 @@ public Task> SearchAsync(string searchText, SearchFilter = searchFilter, }; - return RecommendPackagesAsync(searchToken, cancellationToken); + SearchResult recommenderResults = await RecommendPackagesAsync(searchToken, cancellationToken); + SearchResult decoratedResults = await _decoratedPackageFeed.SearchAsync(searchText, searchFilter, cancellationToken); + + // Add the recommended results to the top of the decorated feed's results, deduplicating any packages in the decorated feed. + IReadOnlyList combinedResults = recommenderResults.Items.Union(decoratedResults.Items, new IdentityIdEqualityComparer()).ToList(); + SearchResult result = SearchResult.FromItems(combinedResults); + // We want to make sure we can continue searching the decorated feed and its sources' search statuses are accurately represented. + result.NextToken = decoratedResults.NextToken; + result.SourceSearchStatus = decoratedResults.SourceSearchStatus; + return result; } private async Task> RecommendPackagesAsync(ContinuationToken continuationToken, CancellationToken cancellationToken) @@ -178,11 +191,13 @@ private async Task> RecommendPackagesAsync( return result; } + // Recommender has no ContinueSearch functionality, so delegate to the decorated feed public Task> ContinueSearchAsync(ContinuationToken continuationToken, CancellationToken cancellationToken) - => System.Threading.Tasks.Task.FromResult(SearchResult.Empty()); + => _decoratedPackageFeed.ContinueSearchAsync(continuationToken, cancellationToken); + // Recommender has no RefreshSearch functionality, so delegate to the decorated feed public Task> RefreshSearchAsync(RefreshToken refreshToken, CancellationToken cancellationToken) - => System.Threading.Tasks.Task.FromResult(SearchResult.Empty()); + => _decoratedPackageFeed.RefreshSearchAsync(refreshToken, cancellationToken); private async Task GetPackageMetadataAsync(PackageIdentity identity, bool includePrerelease, CancellationToken cancellationToken) { @@ -196,5 +211,17 @@ private async Task GetPackageMetadataAsync(PackageIdenti return new RecommendedPackageSearchMetadata(packageMetadata, VersionInfo); } + private class IdentityIdEqualityComparer : IEqualityComparer + { + public bool Equals(IPackageSearchMetadata x, IPackageSearchMetadata y) + { + return x.Identity.Id.Equals(y.Identity.Id); + } + + public int GetHashCode(IPackageSearchMetadata obj) + { + return obj.Identity.Id.GetHashCode(); + } + } } } diff --git a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/Services/NuGetPackageSearchService.cs b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/Services/NuGetPackageSearchService.cs index e0fa7ebcc6c..53a315f71b0 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/Services/NuGetPackageSearchService.cs +++ b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/Services/NuGetPackageSearchService.cs @@ -16,7 +16,6 @@ using Microsoft.ServiceHub.Framework.Services; using NuGet.Common; using NuGet.Configuration; -using NuGet.PackageManagement.VisualStudio.PackageFeeds; using NuGet.Packaging; using NuGet.Packaging.Core; using NuGet.Protocol; @@ -422,14 +421,14 @@ internal async Task CreatePackageFeedAsync( try { // Recommender needs installed and transitive package lists, but it does not need transitive origins data. - IPackageFeed recommenderFeed = new RecommenderPackageFeed( + return new RecommenderPackageFeed( + packageFeed, sourceRepositories, installedPackageCollection, transitivePackageCollection, targetFrameworks, metadataProvider, logger); - return new CombinedPackageFeed(packageFeed, recommenderFeed); } catch (System.IO.FileNotFoundException) { From 857f311e2984420bd565e119d147828c27467e0b Mon Sep 17 00:00:00 2001 From: Jean-Pierre Briede Date: Wed, 16 Oct 2024 17:18:09 -0700 Subject: [PATCH 3/6] Added tests and uncommented out code --- global.json | 2 +- .../PackageFeeds/RecommenderPackageFeed.cs | 4 +- .../Services/NuGetPackageSearchService.cs | 18 +- .../Feeds/RecommenderPackageFeedTests.cs | 164 ++++++++++++++++++ .../NuGetPackageSearchServiceTests.cs | 4 +- 5 files changed, 178 insertions(+), 14 deletions(-) create mode 100644 test/NuGet.Clients.Tests/NuGet.PackageManagement.VisualStudio.Test/Feeds/RecommenderPackageFeedTests.cs diff --git a/global.json b/global.json index 39de10966f0..27003d26657 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { "version": "8.0.100", - "rollForward": "latestMajor", + "rollForward": "latestFeature", "allowPrerelease": true }, "msbuild-sdks": { diff --git a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/RecommenderPackageFeed.cs b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/RecommenderPackageFeed.cs index 3c15abbd273..93c5384c45c 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/RecommenderPackageFeed.cs +++ b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/RecommenderPackageFeed.cs @@ -23,6 +23,8 @@ namespace NuGet.PackageManagement.VisualStudio /// public class RecommenderPackageFeed : IPackageFeed { + private static readonly IdentityIdEqualityComparer IdEqualityComparer = new(); + public bool IsMultiSource => _decoratedPackageFeed.IsMultiSource; private readonly IPackageFeed _decoratedPackageFeed; @@ -102,7 +104,7 @@ public async Task> SearchAsync(string searc SearchResult decoratedResults = await _decoratedPackageFeed.SearchAsync(searchText, searchFilter, cancellationToken); // Add the recommended results to the top of the decorated feed's results, deduplicating any packages in the decorated feed. - IReadOnlyList combinedResults = recommenderResults.Items.Union(decoratedResults.Items, new IdentityIdEqualityComparer()).ToList(); + IReadOnlyList combinedResults = recommenderResults.Items.Union(decoratedResults.Items, IdEqualityComparer).ToList(); SearchResult result = SearchResult.FromItems(combinedResults); // We want to make sure we can continue searching the decorated feed and its sources' search statuses are accurately represented. result.NextToken = decoratedResults.NextToken; diff --git a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/Services/NuGetPackageSearchService.cs b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/Services/NuGetPackageSearchService.cs index 53a315f71b0..e6a3a6ee056 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/Services/NuGetPackageSearchService.cs +++ b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/Services/NuGetPackageSearchService.cs @@ -80,7 +80,7 @@ public async ValueTask> Ge bool recommendPackages = false; IReadOnlyCollection sourceRepositories = await _sharedServiceState.GetRepositoriesAsync(packageSources, cancellationToken); - IPackageFeed packageFeed = await CreatePackageFeedAsync( + IPackageFeed? packageFeed = await CreatePackageFeedAsync( projectContextInfos, targetFrameworks, itemFilter, @@ -263,7 +263,7 @@ public async ValueTask SearchAsync( Assumes.NotNull(searchFilter); IReadOnlyCollection? sourceRepositories = await _sharedServiceState.GetRepositoriesAsync(packageSources, cancellationToken); - IPackageFeed packageFeed = await CreatePackageFeedAsync( + IPackageFeed? packageFeed = await CreatePackageFeedAsync( projectContextInfos, targetFrameworks, itemFilter, @@ -300,7 +300,7 @@ public async ValueTask GetTotalCountAsync( Assumes.NotNull(searchFilter); IReadOnlyCollection? sourceRepositories = await _sharedServiceState.GetRepositoriesAsync(packageSources, cancellationToken); - IPackageFeed packageFeed = await CreatePackageFeedAsync(projectContextInfos, targetFrameworks, itemFilter, isSolution, recommendPackages: false, sourceRepositories, cancellationToken); + IPackageFeed? packageFeed = await CreatePackageFeedAsync(projectContextInfos, targetFrameworks, itemFilter, isSolution, recommendPackages: false, sourceRepositories, cancellationToken); Assumes.NotNull(packageFeed); SourceRepository packagesFolderSourceRepository = await _packagesFolderLocalRepositoryLazy.GetValueAsync(cancellationToken); @@ -382,7 +382,7 @@ public async Task> GetAllPackageFoldersAsync( return allLocalFolders; } - internal async Task CreatePackageFeedAsync( + internal async Task CreatePackageFeedAsync( IReadOnlyCollection projectContextInfos, IReadOnlyCollection targetFrameworks, ItemFilter itemFilter, @@ -458,11 +458,11 @@ internal async Task CreatePackageFeedAsync( return packageFeed; } - //// Search all / updates available cannot work without a source repo - //if (sourceRepositories == null) - //{ - // return packageFeed; - //} + // Search all / updates available cannot work without a source repo + if (sourceRepositories == null) + { + return packageFeed; + } if (itemFilter == ItemFilter.UpdatesAvailable) { diff --git a/test/NuGet.Clients.Tests/NuGet.PackageManagement.VisualStudio.Test/Feeds/RecommenderPackageFeedTests.cs b/test/NuGet.Clients.Tests/NuGet.PackageManagement.VisualStudio.Test/Feeds/RecommenderPackageFeedTests.cs new file mode 100644 index 00000000000..a799aa9ff1b --- /dev/null +++ b/test/NuGet.Clients.Tests/NuGet.PackageManagement.VisualStudio.Test/Feeds/RecommenderPackageFeedTests.cs @@ -0,0 +1,164 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Moq; +using Xunit.Abstractions; +using Xunit; +using NuGet.Test.Utility; +using NuGet.Protocol.Core.Types; +using NuGet.Packaging.Core; +using NuGet.Versioning; +using System.Threading; +using System.Threading.Tasks; + +namespace NuGet.PackageManagement.VisualStudio.Test +{ + public class RecommenderPackageFeedTests + { + private readonly TestLogger _logger; + private readonly SourceRepository _sourceRepository; + private readonly IPackageMetadataProvider _packageMetadataProvider; + + public RecommenderPackageFeedTests(ITestOutputHelper testOutputHelper) + { + _logger = new TestLogger(testOutputHelper); + + var _metadataResource = Mock.Of(); + INuGetResourceProvider provider = FeedTestUtils.CreateTestResourceProvider(_metadataResource); + var packageSource = new Configuration.PackageSource("http://fake-source"); + _sourceRepository = new SourceRepository(source: packageSource, providers: new[] { provider }); + + _packageMetadataProvider = new MultiSourcePackageMetadataProvider(sourceRepositories: [_sourceRepository], optionalLocalRepository: null, optionalGlobalLocalRepositories: null, logger: _logger); + } + + [Fact] + public void Constructor_WithNullArgument_ThrowsArgumentNullException() + { + Assert.Throws(() => + { + var feed = new RecommenderPackageFeed( + decoratedFeed: null, + sourceRepositories: It.IsAny>(), + installedPackages: It.IsAny(), + transitivePackages: It.IsAny(), + targetFrameworks: It.IsAny>(), + metadataProvider: It.IsAny(), + logger: _logger); + }); + + Assert.Throws(() => + { + var feed = new RecommenderPackageFeed( + decoratedFeed: It.IsAny(), + sourceRepositories: null, + installedPackages: It.IsAny(), + transitivePackages: It.IsAny(), + targetFrameworks: It.IsAny>(), + metadataProvider: It.IsAny(), + logger: _logger); + }); + + Assert.Throws(() => + { + var feed = new RecommenderPackageFeed( + decoratedFeed: It.IsAny(), + sourceRepositories: It.IsAny>(), + installedPackages: null, + transitivePackages: It.IsAny(), + targetFrameworks: It.IsAny>(), + metadataProvider: It.IsAny(), + logger: _logger); + }); + + Assert.Throws(() => + { + var feed = new RecommenderPackageFeed( + decoratedFeed: It.IsAny(), + sourceRepositories: It.IsAny>(), + installedPackages: It.IsAny(), + transitivePackages: null, + targetFrameworks: It.IsAny>(), + metadataProvider: It.IsAny(), + logger: _logger); + }); + + Assert.Throws(() => + { + var feed = new RecommenderPackageFeed( + decoratedFeed: It.IsAny(), + sourceRepositories: It.IsAny>(), + installedPackages: It.IsAny(), + transitivePackages: It.IsAny(), + targetFrameworks: null, + metadataProvider: It.IsAny(), + logger: _logger); + }); + + Assert.Throws(() => + { + var feed = new RecommenderPackageFeed( + decoratedFeed: It.IsAny(), + sourceRepositories: It.IsAny>(), + installedPackages: It.IsAny(), + transitivePackages: It.IsAny(), + targetFrameworks: It.IsAny>(), + metadataProvider: null, + logger: _logger); + }); + + Assert.Throws(() => + { + var feed = new RecommenderPackageFeed( + decoratedFeed: It.IsAny(), + sourceRepositories: It.IsAny>(), + installedPackages: It.IsAny(), + transitivePackages: It.IsAny(), + targetFrameworks: It.IsAny>(), + metadataProvider: It.IsAny(), + logger: null); + }); + } + + [Fact] + public async Task SearchAsync_WhenNoRecommendedPackages_ReturnsPackagesFromDecoratedFeedAsync() + { + // Arrange + var decoratedFeed = new Mock(); + var feed = new RecommenderPackageFeed( + decoratedFeed.Object, + sourceRepositories: [_sourceRepository], + installedPackages: new PackageCollection([]), + transitivePackages: new PackageCollection([]), + targetFrameworks: [string.Empty], + metadataProvider: _packageMetadataProvider, + logger: _logger); + + var expectedPackages = new List() + { + PackageSearchMetadataBuilder + .FromIdentity(new PackageIdentity("packageA", new NuGetVersion("1.0.0"))) + .Build(), + PackageSearchMetadataBuilder + .FromIdentity(new PackageIdentity("packageB", new NuGetVersion("2.0.0"))) + .Build(), + PackageSearchMetadataBuilder + .FromIdentity(new PackageIdentity("packageC", new NuGetVersion("3.0.0"))) + .Build() + }; + + decoratedFeed.Setup(f => f.SearchAsync( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(new SearchResult { Items = expectedPackages }); + + // Act + var actualPackages = await feed.SearchAsync(string.Empty, It.IsAny(), It.IsAny()); + + // Assert + Assert.Equal(expectedPackages, actualPackages.Items); + } + } +} diff --git a/test/NuGet.Clients.Tests/NuGet.PackageManagement.VisualStudio.Test/Services/NuGetPackageSearchServiceTests.cs b/test/NuGet.Clients.Tests/NuGet.PackageManagement.VisualStudio.Test/Services/NuGetPackageSearchServiceTests.cs index 9e012d6d21a..5bae4bb7e92 100644 --- a/test/NuGet.Clients.Tests/NuGet.PackageManagement.VisualStudio.Test/Services/NuGetPackageSearchServiceTests.cs +++ b/test/NuGet.Clients.Tests/NuGet.PackageManagement.VisualStudio.Test/Services/NuGetPackageSearchServiceTests.cs @@ -11,7 +11,6 @@ using Microsoft.ServiceHub.Framework; using Microsoft.ServiceHub.Framework.Services; using Microsoft.VisualStudio.ComponentModelHost; -using Microsoft.VisualStudio.PlatformUI; using Microsoft.VisualStudio.Sdk.TestFramework; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -22,7 +21,6 @@ using NuGet.Commands; using NuGet.Common; using NuGet.Configuration; -using NuGet.PackageManagement.VisualStudio.PackageFeeds; using NuGet.Packaging.Core; using NuGet.ProjectManagement; using NuGet.Protocol; @@ -404,7 +402,7 @@ public async Task CreatePackageFeedAsync_WithTransitiveOrigins_OnlyInstalledFeed // Assert Assert.IsType(expectedFeedType, packageFeed); - Assert.IsNotType(packageFeed); + Assert.IsNotType(packageFeed); } private NuGetPackageSearchService SetupSearchService() From 422493ef9bea0b5ee4b85e0c1895d6b7542b8c54 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Briede Date: Wed, 16 Oct 2024 17:19:41 -0700 Subject: [PATCH 4/6] Undo global.json change --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 27003d26657..39de10966f0 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { "version": "8.0.100", - "rollForward": "latestFeature", + "rollForward": "latestMajor", "allowPrerelease": true }, "msbuild-sdks": { From b3a53f97476bc33ccfd5681c35c3a62f4dfe8643 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Briede Date: Thu, 17 Oct 2024 14:17:36 -0700 Subject: [PATCH 5/6] Fix whitespace error --- .../RecommendedPackageSearchMetadata.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NuGet.Clients/NuGet.VisualStudio.Internal.Contracts/RecommendedPackageSearchMetadata.cs b/src/NuGet.Clients/NuGet.VisualStudio.Internal.Contracts/RecommendedPackageSearchMetadata.cs index d5a2796a6af..4eff970750e 100644 --- a/src/NuGet.Clients/NuGet.VisualStudio.Internal.Contracts/RecommendedPackageSearchMetadata.cs +++ b/src/NuGet.Clients/NuGet.VisualStudio.Internal.Contracts/RecommendedPackageSearchMetadata.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; From 057ba14867d93bc09b7d8f1611906612720c1fd1 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Briede Date: Fri, 18 Oct 2024 14:34:58 -0700 Subject: [PATCH 6/6] Make equality comparer static instance --- .../PackageFeeds/RecommenderPackageFeed.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/RecommenderPackageFeed.cs b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/RecommenderPackageFeed.cs index 93c5384c45c..f96adb862a0 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/RecommenderPackageFeed.cs +++ b/src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/PackageFeeds/RecommenderPackageFeed.cs @@ -23,8 +23,6 @@ namespace NuGet.PackageManagement.VisualStudio /// public class RecommenderPackageFeed : IPackageFeed { - private static readonly IdentityIdEqualityComparer IdEqualityComparer = new(); - public bool IsMultiSource => _decoratedPackageFeed.IsMultiSource; private readonly IPackageFeed _decoratedPackageFeed; @@ -104,7 +102,7 @@ public async Task> SearchAsync(string searc SearchResult decoratedResults = await _decoratedPackageFeed.SearchAsync(searchText, searchFilter, cancellationToken); // Add the recommended results to the top of the decorated feed's results, deduplicating any packages in the decorated feed. - IReadOnlyList combinedResults = recommenderResults.Items.Union(decoratedResults.Items, IdEqualityComparer).ToList(); + IReadOnlyList combinedResults = recommenderResults.Items.Union(decoratedResults.Items, IdentityIdEqualityComparer.Instance).ToList(); SearchResult result = SearchResult.FromItems(combinedResults); // We want to make sure we can continue searching the decorated feed and its sources' search statuses are accurately represented. result.NextToken = decoratedResults.NextToken; @@ -215,6 +213,12 @@ private async Task GetPackageMetadataAsync(PackageIdenti private class IdentityIdEqualityComparer : IEqualityComparer { + public static IdentityIdEqualityComparer Instance = new(); + + private IdentityIdEqualityComparer() + { + } + public bool Equals(IPackageSearchMetadata x, IPackageSearchMetadata y) { return x.Identity.Id.Equals(y.Identity.Id);