Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 17 additions & 16 deletions Sources/PackageRegistry/RegistryClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -397,8 +397,8 @@ public final class RegistryClient: AsyncCancellable {
timeout: DispatchTimeInterval?,
observabilityScope: ObservabilityScope
) async throws -> Serialization.VersionMetadata {
let cacheKey = MetadataCacheKey(registry: registry, package: package)
if let cached = self.metadataCache[cacheKey], cached.expires < .now() {
let cacheKey = MetadataCacheKey(registry: registry, package: package, version: version)
if let cached = self.metadataCache[cacheKey], cached.expires > .now() {
return cached.metadata
}

Expand Down Expand Up @@ -1403,21 +1403,9 @@ public final class RegistryClient: AsyncCancellable {
}
}

private func unwrapRegistry(from package: PackageIdentity) throws -> (PackageIdentity.RegistryIdentity, Registry) {
guard let registryIdentity = package.registry else {
throw RegistryError.invalidPackageIdentity(package)
}

guard let registry = self.configuration.registry(for: registryIdentity.scope) else {
throw RegistryError.registryNotConfigured(scope: registryIdentity.scope)
}

return (registryIdentity, registry)
}
Comment on lines -1406 to -1416
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section is just moved below to group internal scoped functions.


// If the registry is available, the function returns, otherwise an error
// explaining why the registry is unavailable is thrown.
private func withAvailabilityCheck(
func withAvailabilityCheck(
registry: Registry,
observabilityScope: ObservabilityScope
) async throws {
Expand All @@ -1438,7 +1426,7 @@ public final class RegistryClient: AsyncCancellable {
}
}

if let cached = self.availabilityCache[registry.url], cached.expires < .now() {
if let cached = self.availabilityCache[registry.url], cached.expires > .now() {
return try availabilityHandler(cached.status)
}

Expand All @@ -1453,6 +1441,18 @@ public final class RegistryClient: AsyncCancellable {
return try availabilityHandler(result)
}

private func unwrapRegistry(from package: PackageIdentity) throws -> (PackageIdentity.RegistryIdentity, Registry) {
guard let registryIdentity = package.registry else {
throw RegistryError.invalidPackageIdentity(package)
}

guard let registry = self.configuration.registry(for: registryIdentity.scope) else {
throw RegistryError.registryNotConfigured(scope: registryIdentity.scope)
}

return (registryIdentity, registry)
}

private func unexpectedStatusError(
_ response: HTTPClientResponse,
expectedStatus: [Int]
Expand Down Expand Up @@ -1495,6 +1495,7 @@ public final class RegistryClient: AsyncCancellable {
private struct MetadataCacheKey: Hashable {
let registry: Registry
let package: PackageIdentity.RegistryIdentity
let version: Version
}
}

Expand Down
203 changes: 202 additions & 1 deletion Tests/PackageRegistryTests/RegistryClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//
//===----------------------------------------------------------------------===//

import Basics
@testable import Basics
import _Concurrency
import Foundation
import PackageFingerprint
Expand Down Expand Up @@ -440,6 +440,126 @@ fileprivate var availabilityURL = URL("\(registryURL)/availability")
assert(metadataSync)
}

@Test func getPackageVersionMetadataInCache() async throws {
let checksumAlgorithm: HashAlgorithm = MockHashAlgorithm()
let expectedChecksums: [Version: String] = [
Version("1.1.1"): "a2ac54cf25fbc1ad0028f03f0aa4b96833b83bb05a14e510892bb27dea4dc812",
Version("1.1.0"): checksumAlgorithm.hash(emptyZipFile).hexadecimalRepresentation
]

let counter = SendableBox(0)
let handler: HTTPClient.Implementation = { request, _ in
await counter.increment()
switch (request.method, request.url) {
case (.get, releasesURL.appending(component: "1.1.1")):
let expectedChecksum = expectedChecksums[Version("1.1.1")]!
#expect(request.headers.get("Accept").first == "application/vnd.swift.registry.v1+json")

let data = """
{
"id": "mona.LinkedList",
"version": "1.1.1",
"resources": [
{
"name": "source-archive",
"type": "application/zip",
"checksum": "\(expectedChecksum)"
}
],
"metadata": {
"author": {
"name": "J. Appleseed"
},
"licenseURL": "https://github.com/mona/LinkedList/license",
"readmeURL": "https://github.com/mona/LinkedList/readme",
"repositoryURLs": [
"https://github.com/mona/LinkedList",
"ssh://git@github.com:mona/LinkedList.git",
"git@github.com:mona/LinkedList.git"
]
}
}
""".data(using: .utf8)!

return .init(
statusCode: 200,
headers: .init([
.init(name: "Content-Length", value: "\(data.count)"),
.init(name: "Content-Type", value: "application/json"),
.init(name: "Content-Version", value: "1"),
]),
body: data
)
case (.get, releasesURL.appending(component: "1.1.0")):
let expectedChecksum = expectedChecksums[Version("1.1.0")]!
#expect(request.headers.get("Accept").first == "application/vnd.swift.registry.v1+json")

let data = """
{
"id": "mona.LinkedList",
"version": "1.1.0",
"resources": [
{
"name": "source-archive",
"type": "application/zip",
"checksum": "\(expectedChecksum)",
}
],
"metadata": {
"author": {
"name": "J. Appleseed"
},
"licenseURL": "https://github.com/mona/LinkedList/license",
"readmeURL": "https://github.com/mona/LinkedList/readme",
"repositoryURLs": [
"https://github.com/mona/LinkedList",
"ssh://git@github.com:mona/LinkedList.git",
"git@github.com:mona/LinkedList.git"
]
}
}
""".data(using: .utf8)!

return .init(
statusCode: 200,
headers: .init([
.init(name: "Content-Length", value: "\(data.count)"),
.init(name: "Content-Type", value: "application/json"),
.init(name: "Content-Version", value: "1"),
]),
body: data
)
default:
throw StringError("method and url should match")
}
}

let httpClient = HTTPClient(implementation: handler)
var configuration = RegistryConfiguration()
configuration.defaultRegistry = Registry(url: registryURL, supportsAvailability: false)

let registryClient = makeRegistryClient(configuration: configuration, httpClient: httpClient)

var expectedRequestCount = 0
try await check(version: Version("1.1.1"), expectCached: false)
try await check(version: Version("1.1.0"), expectCached: false)
try await check(version: Version("1.1.1"), expectCached: true)
try await check(version: Version("1.1.0"), expectCached: true)

func check(version: Version, expectCached: Bool) async throws {
let metadata = try await registryClient.getPackageVersionMetadata(package: identity, version: version)

if !expectCached {
expectedRequestCount += 1
}

let count = await counter.value
#expect(count == expectedRequestCount)
#expect(metadata.author?.name == "J. Appleseed")
#expect(metadata.resources[0].checksum == expectedChecksums[version]!)
}
}

func getPackageVersionMetadata_404() async throws {
let serverErrorHandler = ServerErrorHandler(
method: .get,
Expand Down Expand Up @@ -3701,6 +3821,87 @@ fileprivate var availabilityURL = URL("\(registryURL)/availability")
try await registryClient.checkAvailability(registry: registry)
}
}

@Test func withAvailabilityCheck() async throws {
let handler: HTTPClient.Implementation = { request, _ in
switch (request.method, request.url) {
case (.get, availabilityURL):
return .okay()
default:
throw StringError("method and url should match")
}
}

let httpClient = HTTPClient(implementation: handler)
let registry = Registry(url: registryURL, supportsAvailability: true)

let registryClient = makeRegistryClient(
configuration: .init(),
httpClient: httpClient
)

try await registryClient.withAvailabilityCheck(
registry: registry,
observabilityScope: ObservabilitySystem.NOOP
)
}

@Test func withAvailabilityCheckServerError() async throws {
let handler: HTTPClient.Implementation = { request, _ in
switch (request.method, request.url) {
case (.get, availabilityURL):
return .serverError(reason: "boom")
default:
throw StringError("method and url should match")
}
}

let httpClient = HTTPClient(implementation: handler)
let registry = Registry(url: registryURL, supportsAvailability: true)

let registryClient = makeRegistryClient(
configuration: .init(),
httpClient: httpClient
)

await #expect(throws: StringError("unknown server error (500)")) {
try await registryClient.withAvailabilityCheck(
registry: registry,
observabilityScope: ObservabilitySystem.NOOP
)
}
}

@Test func withAvailabilityCheckInCache() async throws {
let counter = SendableBox(0)
let handler: HTTPClient.Implementation = { request, _ in
await counter.increment()
switch (request.method, request.url) {
case (.get, availabilityURL):
return .okay()
default:
throw StringError("method and url should match")
}
}

let httpClient = HTTPClient(implementation: handler)
let registry = Registry(url: registryURL, supportsAvailability: true)

let registryClient = makeRegistryClient(
configuration: .init(),
httpClient: httpClient
)

// Request count should not increase after first check
for _ in 0..<5 {
try await registryClient.withAvailabilityCheck(
registry: registry,
observabilityScope: ObservabilitySystem.NOOP
)
let count = await counter.value
#expect(count == 1)
}
}
}

// MARK: - Sugar
Expand Down