From de155d8a7288a85d983e89e263776b4d557bad18 Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Mon, 21 Jun 2021 21:25:08 -0700 Subject: [PATCH] [Collections] Add listPackages API (#3566) * [Collections] Add listPackages API rdar://79528550 * Make 'collections' param optional --- Sources/PackageCollections/API.swift | 10 ++ .../PackageCollections.swift | 34 +++++++ .../PackageCollectionsTests.swift | 96 +++++++++++++++++++ 3 files changed, 140 insertions(+) diff --git a/Sources/PackageCollections/API.swift b/Sources/PackageCollections/API.swift index 6edf1347214..bd1c6172576 100644 --- a/Sources/PackageCollections/API.swift +++ b/Sources/PackageCollections/API.swift @@ -132,6 +132,16 @@ public protocol PackageCollectionsProtocol { callback: @escaping (Result) -> Void ) + /// Lists packages from the specified collections. + /// + /// - Parameters: + /// - collections: Optional. If specified, only packages in these collections are included. + /// - callback: The closure to invoke when result becomes available + func listPackages( + collections: Set?, + callback: @escaping (Result) -> Void + ) + // MARK: - Target (Module) APIs /// List all known targets. diff --git a/Sources/PackageCollections/PackageCollections.swift b/Sources/PackageCollections/PackageCollections.swift index 62ec618cfd5..a46574436a2 100644 --- a/Sources/PackageCollections/PackageCollections.swift +++ b/Sources/PackageCollections/PackageCollections.swift @@ -307,6 +307,40 @@ public struct PackageCollections: PackageCollectionsProtocol { } } + public func listPackages(collections: Set? = nil, + callback: @escaping (Result) -> Void) { + self.listCollections(identifiers: collections) { result in + switch result { + case .failure(let error): + callback(.failure(error)) + case .success(let collections): + var packageCollections = [PackageReference: (package: Model.Package, collections: Set)]() + // Use package data from the most recently processed collection + collections.sorted(by: { $0.lastProcessedAt > $1.lastProcessedAt }).forEach { collection in + collection.packages.forEach { package in + var entry = packageCollections.removeValue(forKey: package.reference) + if entry == nil { + entry = (package, .init()) + } + + if var entry = entry { + entry.collections.insert(collection.identifier) + packageCollections[package.reference] = entry + } + } + } + + let result = PackageCollectionsModel.PackageSearchResult( + items: packageCollections.sorted { $0.key.identity < $1.key.identity } + .map { entry in + .init(package: entry.value.package, collections: Array(entry.value.collections)) + } + ) + callback(.success(result)) + } + } + } + // MARK: - Package Metadata public func getPackageMetadata(_ reference: PackageReference, diff --git a/Tests/PackageCollectionsTests/PackageCollectionsTests.swift b/Tests/PackageCollectionsTests/PackageCollectionsTests.swift index aefa2f688d9..397c24922d4 100644 --- a/Tests/PackageCollectionsTests/PackageCollectionsTests.swift +++ b/Tests/PackageCollectionsTests/PackageCollectionsTests.swift @@ -1486,6 +1486,102 @@ final class PackageCollectionsTests: XCTestCase { let delta = Date().timeIntervalSince(start) XCTAssert(delta < 1.0, "should fetch quickly, took \(delta)") } + + func testListPackages() throws { + try skipIfUnsupportedPlatform() + + let configuration = PackageCollections.Configuration() + let storage = makeMockStorage() + defer { XCTAssertNoThrow(try storage.close()) } + + var mockCollections = makeMockCollections(count: 5) + + let mockTargets = [UUID().uuidString, UUID().uuidString].map { + PackageCollectionsModel.Target(name: $0, moduleName: $0) + } + + let mockProducts = [PackageCollectionsModel.Product(name: UUID().uuidString, type: .executable, targets: [mockTargets.first!]), + PackageCollectionsModel.Product(name: UUID().uuidString, type: .executable, targets: mockTargets)] + let toolsVersion = ToolsVersion(string: "5.2")! + let mockManifest = PackageCollectionsModel.Package.Version.Manifest( + toolsVersion: toolsVersion, + packageName: UUID().uuidString, + targets: mockTargets, + products: mockProducts, + minimumPlatformVersions: nil + ) + + let mockVersion = PackageCollectionsModel.Package.Version(version: TSCUtility.Version(1, 0, 0), + title: nil, + summary: nil, + manifests: [toolsVersion: mockManifest], + defaultToolsVersion: toolsVersion, + verifiedCompatibility: nil, + license: nil, + createdAt: nil) + + let mockPackage = PackageCollectionsModel.Package(repository: .init(url: "https://packages.mock/\(UUID().uuidString)"), + summary: UUID().uuidString, + keywords: [UUID().uuidString, UUID().uuidString], + versions: [mockVersion], + watchersCount: nil, + readmeURL: nil, + license: nil, + authors: nil, + languages: nil) + + let mockCollection = PackageCollectionsModel.Collection(source: .init(type: .json, url: URL(string: "https://feed.mock/\(UUID().uuidString)")!), + name: UUID().uuidString, + overview: UUID().uuidString, + keywords: [UUID().uuidString, UUID().uuidString], + packages: [mockPackage], + createdAt: Date(), + createdBy: nil, + signature: nil) + + let mockCollection2 = PackageCollectionsModel.Collection(source: .init(type: .json, url: URL(string: "https://feed.mock/\(UUID().uuidString)")!), + name: UUID().uuidString, + overview: UUID().uuidString, + keywords: [UUID().uuidString, UUID().uuidString], + packages: [mockPackage], + createdAt: Date(), + createdBy: nil, + signature: nil) + + mockCollections.append(mockCollection) + mockCollections.append(mockCollection2) + + let collectionProviders = [PackageCollectionsModel.CollectionSourceType.json: MockCollectionsProvider(mockCollections)] + let metadataProvider = MockMetadataProvider([:]) + let packageCollections = PackageCollections(configuration: configuration, storage: storage, collectionProviders: collectionProviders, metadataProvider: metadataProvider) + + try mockCollections.forEach { collection in + _ = try tsc_await { callback in packageCollections.addCollection(collection.source, trustConfirmationProvider: { _, cb in cb(true) }, callback: callback) } + } + + do { + let fetchCollections = Set(mockCollections.map { $0.identifier } + [mockCollection.identifier, mockCollection2.identifier]) + let expectedPackages = Set(mockCollections.flatMap { $0.packages.map { $0.reference } } + [mockPackage.reference]) + let expectedCollections = Set([mockCollection.identifier, mockCollection2.identifier]) + + let searchResult = try tsc_await { callback in packageCollections.listPackages(collections: fetchCollections, callback: callback) } + XCTAssertEqual(searchResult.items.count, expectedPackages.count, "list count should match") + XCTAssertEqual(Set(searchResult.items.map { $0.package.reference }), expectedPackages, "items should match") + XCTAssertEqual(Set(searchResult.items.first(where: { $0.package.reference == mockPackage.reference })?.collections ?? []), expectedCollections, "collections should match") + } + + // Call API for specific collections + do { + let fetchCollections = Set([mockCollections[0].identifier, mockCollection.identifier, mockCollection2.identifier]) + let expectedPackages = Set(mockCollections[0].packages.map { $0.reference } + [mockPackage.reference]) + let expectedCollections = Set([mockCollection.identifier, mockCollection2.identifier]) + + let searchResult = try tsc_await { callback in packageCollections.listPackages(collections: fetchCollections, callback: callback) } + XCTAssertEqual(searchResult.items.count, expectedPackages.count, "list count should match") + XCTAssertEqual(Set(searchResult.items.map { $0.package.reference }), expectedPackages, "items should match") + XCTAssertEqual(Set(searchResult.items.first(where: { $0.package.reference == mockPackage.reference })?.collections ?? []), expectedCollections, "collections should match") + } + } } private extension XCTestCase {