Skip to content

Commit

Permalink
Add PackageTests
Browse files Browse the repository at this point in the history
  • Loading branch information
finestructure committed Feb 14, 2024
1 parent cb6c84b commit 4d23075
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 38 deletions.
21 changes: 17 additions & 4 deletions Sources/ValidatorCore/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ import NIO


struct Environment {
var decodeManifest: (_ client: HTTPClient, _ repository: Github.Repository) async throws -> Package
var decodeManifest: (_ client: Client, _ repository: Github.Repository) async throws -> Package
var fileManager: FileManager
var fetch: (_ client: HTTPClient, _ url: URL) -> EventLoopFuture<ByteBuffer>
var fetch: (_ client: Client, _ url: URL) -> EventLoopFuture<ByteBuffer>
var fetchDependencies: (_ api: SwiftPackageIndexAPI) async throws -> [SwiftPackageIndexAPI.PackageRecord]
var fetchRepository: (_ client: HTTPClient, _ url: PackageURL) async throws -> Github.Repository
var fetchRepository: (_ client: Client, _ url: PackageURL) async throws -> Github.Repository
var githubToken: () -> String?
var resolvePackageRedirects: (_ client: HTTPClient, _ url: PackageURL) async throws -> Redirect
var resolvePackageRedirects: (_ client: Client, _ url: PackageURL) async throws -> Redirect
var shell: Shell
}

Expand Down Expand Up @@ -55,6 +55,19 @@ extension Environment {
}


protocol Client {
var eventLoopGroup: EventLoopGroup { get }
func execute(request: HTTPClient.Request, deadline: NIODeadline?) -> EventLoopFuture<HTTPClient.Response>
}
extension Client {
func execute(request: HTTPClient.Request) -> EventLoopFuture<HTTPClient.Response> {
execute(request: request, deadline: nil)
}
}

extension HTTPClient: Client { }


#if DEBUG
var Current: Environment = .live
#else
Expand Down
10 changes: 5 additions & 5 deletions Sources/ValidatorCore/Github.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,12 @@ extension Github {
static var repositoryCache = Cache<Repository>()


static func fetchRepository(client: HTTPClient, url: PackageURL) async throws-> Repository {
static func fetchRepository(client: Client, url: PackageURL) async throws-> Repository {
try await fetchRepository(client: client, url: url, attempt: 0)
}


static func fetchRepository(client: HTTPClient, url: PackageURL, attempt: Int) async throws-> Repository {
static func fetchRepository(client: Client, url: PackageURL, attempt: Int) async throws-> Repository {
guard attempt < 3 else { throw AppError.retryLimitExceeded }
let apiURL = URL(string: "https://api.github.com/repos/\(url.owner)/\(url.repository)")!
let key = Cache<Repository>.Key(string: apiURL.absoluteString)
Expand All @@ -158,7 +158,7 @@ extension Github {
}


static func listRepositoryFilePaths(client: HTTPClient, repository: Repository) async throws -> [String] {
static func listRepositoryFilePaths(client: Client, repository: Repository) async throws -> [String] {
let apiURL = URL( string: "https://api.github.com/repos/\(repository.path)/git/trees/\(repository.defaultBranch)" )!
struct Response: Decodable {
var tree: [File]
Expand All @@ -180,7 +180,7 @@ extension Github {
}


static func fetch<T: Decodable>(_ type: T.Type, client: HTTPClient, url: URL) async throws -> T {
static func fetch<T: Decodable>(_ type: T.Type, client: Client, url: URL) async throws -> T {
let body = try await Current.fetch(client, url).get()
do {
return try JSONDecoder().decode(type, from: body)
Expand All @@ -192,7 +192,7 @@ extension Github {
}
}

static func fetch(client: HTTPClient, url: URL) -> EventLoopFuture<ByteBuffer> {
static func fetch(client: Client, url: URL) -> EventLoopFuture<ByteBuffer> {
let eventLoop = client.eventLoopGroup.next()
guard let token = Current.githubToken() else {
return eventLoop.makeFailedFuture(AppError.githubTokenNotSet)
Expand Down
9 changes: 6 additions & 3 deletions Sources/ValidatorCore/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,18 @@ extension Package {
static func loadPackageDumpCache() { packageDumpCache = .load(from: cacheFilename) }
static func savePackageDumpCache() throws { try packageDumpCache.save(to: cacheFilename) }

static func decode(client: HTTPClient, repository: Github.Repository) async throws -> Self {
static func decode(client: Client, repository: Github.Repository) async throws -> Self {
let cacheKey = repository.path
if let cached = packageDumpCache[Cache.Key(string: cacheKey)] {
return cached
}
return try await withTempDir { tempDir in
for manifestURL in try await Package.getManifestURLs(client: client, repository: repository) {
let fileURL = URL(fileURLWithPath: tempDir).appendingPathComponent(manifestURL.lastPathComponent)
let data = try Data(contentsOf: manifestURL.rawValue)
let buffer = try await Current.fetch(client, manifestURL.rawValue).get()
guard let data = buffer.getData(at: 0, length: buffer.readableBytes) else {
throw AppError.dumpPackageError("failed to get data for manifest \(manifestURL.rawValue.absoluteString)")
}
guard Current.fileManager.createFile(fileURL.path, data, nil) else {
throw AppError.dumpPackageError("failed to save manifest \(manifestURL.rawValue.absoluteString) to temp directory \(fileURL.absoluteString)")
}
Expand All @@ -121,7 +124,7 @@ extension Package {
enum Manifest {}
typealias ManifestURL = Tagged<Manifest, URL>

static func getManifestURLs(client: HTTPClient, repository: Github.Repository) async throws -> [ManifestURL] {
static func getManifestURLs(client: Client, repository: Github.Repository) async throws -> [ManifestURL] {
let manifestFiles = try await Github.listRepositoryFilePaths(client: client, repository: repository)
.filter { $0.hasPrefix("Package") }
.filter { $0.hasSuffix(".swift") }
Expand Down
6 changes: 3 additions & 3 deletions Sources/ValidatorCore/RedirectFollower.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ enum Redirect: Equatable {
}


private func resolveRedirects(client: HTTPClient, for url: PackageURL) async throws -> Redirect {
private func resolveRedirects(client: Client, for url: PackageURL) async throws -> Redirect {
var lastResult = Redirect.initial(url)
var hopCount = 0
let maxHops = 10

func _resolveRedirects(client: HTTPClient, for url: PackageURL) async throws -> Redirect {
func _resolveRedirects(client: Client, for url: PackageURL) async throws -> Redirect {
var request = try HTTPClient.Request(url: url.rawValue, method: .HEAD, headers: .init([
("User-Agent", "SPI-Validator")
]))
Expand Down Expand Up @@ -103,7 +103,7 @@ private func resolveRedirects(client: HTTPClient, for url: PackageURL) async thr
}


func resolvePackageRedirects(client: HTTPClient, for url: PackageURL) async throws -> Redirect {
func resolvePackageRedirects(client: Client, for url: PackageURL) async throws -> Redirect {
let res = try await resolveRedirects(client: client, for: url.deletingGitExtension())
switch res {
case .initial, .notFound, .error, .unauthorized, .rateLimited:
Expand Down
23 changes: 0 additions & 23 deletions Tests/ValidatorTests/CheckDependenciesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -224,26 +224,3 @@ private extension SwiftPackageIndexAPI.PackageRecord {
self.init(id: .init(), url: url, resolvedDependencies: dependencies)
}
}

private extension Data {
static func listRepositoryFilePaths(for path: String) -> Self {
.init("""
{
"url" : "https://api.github.com/repos/\(path)/git/trees/ea8eea9d89842a29af1b8e6c7677f1c86e72fa42",
"tree" : [
{
"size" : 1122,
"type" : "blob",
"path" : "Package.swift",
"url" : "https://api.github.com/repos/\(path)/git/blobs/bf4aa0c6a8bd9f749c2f96905c40bf2f70ef97d2",
"mode" : "100644",
"sha" : "bf4aa0c6a8bd9f749c2f96905c40bf2f70ef97d2"
}
],
"sha" : "ea8eea9d89842a29af1b8e6c7677f1c86e72fa42",
"truncated" : false
}
""".utf8
)
}
}
19 changes: 19 additions & 0 deletions Tests/ValidatorTests/Fixtures/SemanticVersion-Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// swift-tools-version:5.6

import PackageDescription

let package = Package(
name: "SemanticVersion",
products: [
.library(
name: "SemanticVersion",
targets: ["SemanticVersion"]),
],
dependencies: [],
targets: [
.target(name: "SemanticVersion",
dependencies: [],
resources: [.process("Documentation.docc")]),
.testTarget(name: "SemanticVersionTests", dependencies: ["SemanticVersion"]),
]
)
98 changes: 98 additions & 0 deletions Tests/ValidatorTests/PackageTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import XCTest

@testable import ValidatorCore

import AsyncHTTPClient
import NIO
import NIOHTTP1


final class PackageTests: XCTestCase {

func test_decode_multiple_manifests() async throws {
// This tests package dump for a package with four versioned package manifest files.
// We use a captured response for the SwiftyJSON package which lists four manifest files
// and then save no data for three of them that shouldn't be used and mock in the
// SemanticVersion manifest files for the one that should be decoded.
// setup
Current = .mock
Current.fileManager = .live
var manifestsFetched = 0
Current.fetch = { client, url in
switch url.absoluteString {
case "https://raw.githubusercontent.com/org/1/main/[email protected]":
// Package.decode -> fetch manifestURL data
manifestsFetched += 1
return client.eventLoopGroup.next().makeSucceededFuture(
try! .fixture(for: "SemanticVersion-Package.swift")
)
case "https://raw.githubusercontent.com/org/1/main/Package.swift",
"https://raw.githubusercontent.com/org/1/main/[email protected]",
"https://raw.githubusercontent.com/org/1/main/[email protected]":
// Package.decode -> fetch manifestURL data - save bad data in the unrelated manifests to raise an error if used
manifestsFetched += 1
return client.eventLoopGroup.next().makeSucceededFuture(
.init()
)
case "https://api.github.com/repos/org/1/git/trees/main":
// getManifestURLs -> Github.listRepositoryFilePaths -> Github.fetch
return client.eventLoopGroup.next().makeSucceededFuture(
// github-files-response-SwiftyJSON has multiple manifest files
try! .fixture(for: "github-files-response-SwiftyJSON.json")
)
default:
return client.eventLoopGroup.next().makeFailedFuture(
Error.unexpectedCall("Current.fetch \(url.absoluteString)")
)
}
}
Current.shell = .live

let client = MockClient(response: { .mock(status: .ok) })

// MUT
let pkg = try await Package.decode(client: client, repository: .init(defaultBranch: "main", owner: "org", name: "1"))

// validate
XCTAssertEqual(manifestsFetched, 4)
XCTAssertEqual(pkg.name, "SemanticVersion")
}

}


struct MockClient: Client {
var response: () -> HTTPClient.Response

func execute(request: HTTPClient.Request, deadline: NIODeadline?) -> EventLoopFuture<HTTPClient.Response> {
eventLoopGroup.next().makeSucceededFuture(response())
}

let eventLoopGroup: EventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
}


extension HTTPClient.Response {
static func mock(status: HTTPResponseStatus) -> Self {
.init(host: "host", status: status, version: .http1_1, headers: [:], body: nil)
}
}


private enum Error: Swift.Error {
case unexpectedCall(String)
}
12 changes: 12 additions & 0 deletions Tests/ValidatorTests/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,24 @@
// limitations under the License.

import Foundation
import NIO


func fixtureData(for fixture: String) throws -> Data {
try Data(contentsOf: fixtureUrl(for: fixture))
}

extension Data {
static func fixture(for fileName: String) throws -> Self {
try fixtureData(for: fileName)
}
}

extension ByteBuffer {
static func fixture(for fileName: String) throws -> Self {
try .init(data: .fixture(for: fileName))
}
}

func fixtureUrl(for fixture: String) -> URL {
fixturesDirectory().appendingPathComponent(fixture)
Expand Down

0 comments on commit 4d23075

Please sign in to comment.