diff --git a/Package.resolved b/Package.resolved index b9b9e4620..3c9aaaf61 100644 --- a/Package.resolved +++ b/Package.resolved @@ -46,6 +46,15 @@ "version" : "0.0.8" } }, + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "9fa31f4403da54855f1e2aeaeff478f4f0e40b13", + "version" : "1.0.2" + } + }, { "identity" : "console-kit", "kind" : "remoteSourceControl", @@ -184,7 +193,7 @@ { "identity" : "shellquote", "kind" : "remoteSourceControl", - "location" : "https://github.com/SwiftPackageIndex/ShellQuote", + "location" : "https://github.com/SwiftPackageIndex/ShellQuote.git", "state" : { "revision" : "5f555550c30ef43d64b36b40c2c291a95d62580c", "version" : "1.0.2" @@ -307,6 +316,15 @@ "version" : "1.3.0" } }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "b9b24b69e2adda099a1fa381cda1eeec272d5b53", + "version" : "1.0.5" + } + }, { "identity" : "swift-collections", "kind" : "remoteSourceControl", @@ -316,6 +334,15 @@ "version" : "1.1.4" } }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "6054df64b55186f08b6d0fd87152081b8ad8d613", + "version" : "1.2.0" + } + }, { "identity" : "swift-crypto", "kind" : "remoteSourceControl", @@ -334,6 +361,15 @@ "version" : "1.3.3" } }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "fd1fb25b68fdb9756cd61d23dbd9e2614b340085", + "version" : "1.4.0" + } + }, { "identity" : "swift-driver", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index e280e0081..1b03ae382 100644 --- a/Package.swift +++ b/Package.swift @@ -39,6 +39,7 @@ let package = Package( .package(url: "https://github.com/swiftlang/swift-package-manager.git", branch: "release/5.10"), .package(url: "https://github.com/dankinsoid/VaporToOpenAPI.git", from: "4.4.4"), .package(url: "https://github.com/pointfreeco/swift-custom-dump.git", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0"), .package(url: "https://github.com/pointfreeco/swift-parsing.git", from: "0.12.0"), .package(url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.11.1"), .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.3.2"), @@ -64,6 +65,7 @@ let package = Package( .product(name: "Cache", package: "cache"), .product(name: "CanonicalPackageURL", package: "CanonicalPackageURL"), .product(name: "CustomDump", package: "swift-custom-dump"), + .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "DependencyResolution", package: "DependencyResolution"), .product(name: "Fluent", package: "fluent"), .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"), diff --git a/Sources/App/Commands/Alerting.swift b/Sources/App/Commands/Alerting.swift index 1fa04b145..cd437540a 100644 --- a/Sources/App/Commands/Alerting.swift +++ b/Sources/App/Commands/Alerting.swift @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Dependencies import Fluent +import NIOCore import SQLKit import Vapor -import NIOCore enum Alerting { @@ -117,7 +118,8 @@ extension Alerting { defer { Current.logger().debug("fetchBuilds elapsed: \(Date.now.timeIntervalSince(start).rounded(decimalPlaces: 2))s") } - let cutoff = Current.date().addingTimeInterval(-timePeriod.timeInterval) + @Dependency(\.date.now) var now + let cutoff = now.addingTimeInterval(-timePeriod.timeInterval) let builds = try await Build.query(on: database) .field(\.$createdAt) .field(\.$updatedAt) diff --git a/Sources/App/Commands/Analyze.swift b/Sources/App/Commands/Analyze.swift index 5e7e25dbf..4e3453968 100644 --- a/Sources/App/Commands/Analyze.swift +++ b/Sources/App/Commands/Analyze.swift @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Dependencies import DependencyResolution import Fluent import SPIManifest @@ -84,8 +85,8 @@ extension Analyze { } .forEach { pair in guard let (path, mod) = pair else { return } - let cutoff = Current.date() - .addingTimeInterval(-Constants.gitCheckoutMaxAge) + @Dependency(\.date.now) var now + let cutoff = now.addingTimeInterval(-Constants.gitCheckoutMaxAge) if mod < cutoff { try Current.fileManager.removeItem(atPath: path) AppMetrics.analyzeTrimCheckoutsCount?.inc() @@ -169,45 +170,53 @@ extension Analyze { package: Joined) async throws { try await refreshCheckout(package: package) - try await database.transaction { tx in - try await updateRepository(on: tx, package: package) + // 2024-10-05 sas: We need to explicitly weave dependencies into the `transaction` closure, because escaping closures strip them. + // https://github.com/pointfreeco/swift-dependencies/discussions/283#discussioncomment-10846172 + // This might not be needed in Vapor 5 / FluentKit 2 + // TODO: verify this is still needed once we upgrade to Vapor 5 / FluentKit 2 + try await withEscapedDependencies { dependencies in + try await database.transaction { tx in + try await dependencies.yield { + try await updateRepository(on: tx, package: package) - let versionDelta = try await diffVersions(client: client, transaction: tx, - package: package) - let netDeleteCount = versionDelta.toDelete.count - versionDelta.toAdd.count - if netDeleteCount > 1 { - Current.logger().warning("Suspicious loss of \(netDeleteCount) versions for package \(package.model.id)") - } + let versionDelta = try await diffVersions(client: client, transaction: tx, + package: package) + let netDeleteCount = versionDelta.toDelete.count - versionDelta.toAdd.count + if netDeleteCount > 1 { + Current.logger().warning("Suspicious loss of \(netDeleteCount) versions for package \(package.model.id)") + } - try await applyVersionDelta(on: tx, delta: versionDelta) + try await applyVersionDelta(on: tx, delta: versionDelta) - let newVersions = versionDelta.toAdd + let newVersions = versionDelta.toAdd - mergeReleaseInfo(package: package, into: newVersions) + mergeReleaseInfo(package: package, into: newVersions) - var versionsPkgInfo = [(Version, PackageInfo)]() - for version in newVersions { - if let pkgInfo = try? await getPackageInfo(package: package, version: version) { - versionsPkgInfo.append((version, pkgInfo)) - } - } - if !newVersions.isEmpty && versionsPkgInfo.isEmpty { - throw AppError.noValidVersions(package.model.id, package.model.url) - } + var versionsPkgInfo = [(Version, PackageInfo)]() + for version in newVersions { + if let pkgInfo = try? await getPackageInfo(package: package, version: version) { + versionsPkgInfo.append((version, pkgInfo)) + } + } + if !newVersions.isEmpty && versionsPkgInfo.isEmpty { + throw AppError.noValidVersions(package.model.id, package.model.url) + } - for (version, pkgInfo) in versionsPkgInfo { - try await updateVersion(on: tx, version: version, packageInfo: pkgInfo) - try await recreateProducts(on: tx, version: version, manifest: pkgInfo.packageManifest) - try await recreateTargets(on: tx, version: version, manifest: pkgInfo.packageManifest) - } + for (version, pkgInfo) in versionsPkgInfo { + try await updateVersion(on: tx, version: version, packageInfo: pkgInfo) + try await recreateProducts(on: tx, version: version, manifest: pkgInfo.packageManifest) + try await recreateTargets(on: tx, version: version, manifest: pkgInfo.packageManifest) + } + + let versions = try await updateLatestVersions(on: tx, package: package) - let versions = try await updateLatestVersions(on: tx, package: package) - - let targets = await fetchTargets(on: tx, package: package) + let targets = await fetchTargets(on: tx, package: package) - updateScore(package: package, versions: versions, targets: targets) + updateScore(package: package, versions: versions, targets: targets) - await onNewVersions(client: client, package: package, versions: newVersions) + await onNewVersions(client: client, package: package, versions: newVersions) + } + } } } @@ -410,7 +419,8 @@ extension Analyze { return incoming } - let ageOfExistingVersion = Current.date().timeIntervalSinceReferenceDate - existingVersion.commitDate.timeIntervalSinceReferenceDate + @Dependency(\.date.now) var now + let ageOfExistingVersion = now.timeIntervalSinceReferenceDate - existingVersion.commitDate.timeIntervalSinceReferenceDate let resultingBranchVersion: Version if existingVersion.reference.branchName != incomingVersion.reference.branchName { diff --git a/Sources/App/Commands/ReAnalyzeVersions.swift b/Sources/App/Commands/ReAnalyzeVersions.swift index 6196da13a..f35bfb020 100644 --- a/Sources/App/Commands/ReAnalyzeVersions.swift +++ b/Sources/App/Commands/ReAnalyzeVersions.swift @@ -12,8 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Vapor +import Dependencies import Fluent +import Vapor enum ReAnalyzeVersions { @@ -48,13 +49,14 @@ enum ReAnalyzeVersions { let db = context.application.db Current.setLogger(Logger(component: "re-analyze-versions")) + @Dependency(\.date.now) var now if let id = signature.packageId { Current.logger().info("Re-analyzing versions (id: \(id)) ...") do { try await reAnalyzeVersions( client: client, database: db, - versionsLastUpdatedBefore: Current.date(), + versionsLastUpdatedBefore: now, refreshCheckouts: signature.refreshCheckouts, packageId: id ) @@ -174,38 +176,46 @@ enum ReAnalyzeVersions { for pkg in packages { Current.logger().info("Re-analyzing package \(pkg.model.url) ...") - try await database.transaction { tx in - guard let cacheDir = Current.fileManager.cacheDirectoryPath(for: pkg.model) else { return } - if !Current.fileManager.fileExists(atPath: cacheDir) || refreshCheckouts { - try await Analyze.refreshCheckout(package: pkg) - } - - let versions = try await getExistingVersions(client: client, - transaction: tx, - package: pkg, - before: cutoffDate) - Current.logger().info("Updating \(versions.count) versions (id: \(pkg.model.id)) ...") - - try await setUpdatedAt(on: tx, versions: versions) - - Analyze.mergeReleaseInfo(package: pkg, into: versions) - - for version in versions { - let pkgInfo: Analyze.PackageInfo - do { - pkgInfo = try await Analyze.getPackageInfo(package: pkg, version: version) - } catch { - Current.logger().report(error: error) - continue + // 2024-10-05 sas: We need to explicitly weave dependencies into the `transaction` closure, because escaping closures strip them. + // https://github.com/pointfreeco/swift-dependencies/discussions/283#discussioncomment-10846172 + // This might not be needed in Vapor 5 / FluentKit 2 + // TODO: verify this is still needed once we upgrade to Vapor 5 / FluentKit 2 + try await withEscapedDependencies { dependencies in + try await database.transaction { tx in + try await dependencies.yield { + guard let cacheDir = Current.fileManager.cacheDirectoryPath(for: pkg.model) else { return } + if !Current.fileManager.fileExists(atPath: cacheDir) || refreshCheckouts { + try await Analyze.refreshCheckout(package: pkg) + } + + let versions = try await getExistingVersions(client: client, + transaction: tx, + package: pkg, + before: cutoffDate) + Current.logger().info("Updating \(versions.count) versions (id: \(pkg.model.id)) ...") + + try await setUpdatedAt(on: tx, versions: versions) + + Analyze.mergeReleaseInfo(package: pkg, into: versions) + + for version in versions { + let pkgInfo: Analyze.PackageInfo + do { + pkgInfo = try await Analyze.getPackageInfo(package: pkg, version: version) + } catch { + Current.logger().report(error: error) + continue + } + + try await Analyze.updateVersion(on: tx, version: version, packageInfo: pkgInfo) + try await Analyze.recreateProducts(on: tx, version: version, manifest: pkgInfo.packageManifest) + try await Analyze.recreateTargets(on: tx, version: version, manifest: pkgInfo.packageManifest) + } + + // No need to run `updateLatestVersions` because we're only operating on existing versions, + // not adding any new ones that could change the `latest` marker. } - - try await Analyze.updateVersion(on: tx, version: version, packageInfo: pkgInfo) - try await Analyze.recreateProducts(on: tx, version: version, manifest: pkgInfo.packageManifest) - try await Analyze.recreateTargets(on: tx, version: version, manifest: pkgInfo.packageManifest) } - - // No need to run `updateLatestVersions` because we're only operating on existing versions, - // not adding any new ones that could change the `latest` marker. } } } @@ -225,8 +235,9 @@ enum ReAnalyzeVersions { static func setUpdatedAt(on database: Database, versions: [Version]) async throws { + @Dependency(\.date.now) var now for version in versions { - version.updatedAt = Current.date() + version.updatedAt = now } try await versions.save(on: database) } diff --git a/Sources/App/Controllers/SiteMapController.swift b/Sources/App/Controllers/SiteMapController.swift index 3e867fdaa..41f6bb243 100644 --- a/Sources/App/Controllers/SiteMapController.swift +++ b/Sources/App/Controllers/SiteMapController.swift @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Vapor +import Dependencies import Fluent -import SQLKit import Plot +import SQLKit +import Vapor enum SiteMapController { @@ -67,10 +68,11 @@ enum SiteMapController { /// - Parameter packages: list of packages /// - Returns: `SiteMapIndex` static func index(packages: [SiteMapController.Package]) -> SiteMapIndex { - SiteMapIndex( + @Dependency(\.date.now) var now + return SiteMapIndex( .sitemap( .loc(SiteURL.siteMapStaticPages.absoluteURL()), - .lastmod(Current.date(), timeZone: .utc) // The home page updates every day. + .lastmod(now, timeZone: .utc) // The home page updates every day. ), .group( packages.map { package -> Node in diff --git a/Sources/App/Core/AppEnvironment.swift b/Sources/App/Core/AppEnvironment.swift index f92c0573a..6889c169e 100644 --- a/Sources/App/Core/AppEnvironment.swift +++ b/Sources/App/Core/AppEnvironment.swift @@ -39,7 +39,6 @@ struct AppEnvironment: Sendable { var collectionSigningCertificateChain: @Sendable () -> [URL] var collectionSigningPrivateKey: @Sendable () -> Data? var currentReferenceCache: @Sendable () -> CurrentReferenceCache? - var date: @Sendable () -> Date var dbId: @Sendable () -> String? var environment: @Sendable () -> Environment var fetchDocumentation: @Sendable (_ client: Client, _ url: URI) async throws -> ClientResponse @@ -162,7 +161,6 @@ extension AppEnvironment { .map { Data($0.utf8) } }, currentReferenceCache: { .live }, - date: { .init() }, dbId: { Environment.get("DATABASE_ID") }, environment: { (try? Environment.detect()) ?? .development }, fetchDocumentation: { client, url in try await client.get(url) }, diff --git a/Sources/App/Core/Extensions/Date+ext.swift b/Sources/App/Core/Extensions/Date+ext.swift index 2b49b62fc..1b50deb86 100644 --- a/Sources/App/Core/Extensions/Date+ext.swift +++ b/Sources/App/Core/Extensions/Date+ext.swift @@ -13,6 +13,7 @@ // limitations under the License. import Foundation +import Dependencies extension DateFormatter { @@ -78,5 +79,8 @@ extension Date: Swift.LosslessStringConvertible { extension Date { - var relative: String { "\(date: self, relativeTo: Current.date())" } + var relative: String { + @Dependency(\.date.now) var now + return "\(date: self, relativeTo: now)" + } } diff --git a/Sources/App/Core/Extensions/Result+ext.swift b/Sources/App/Core/Extensions/Result+ext.swift index ab88c419a..e7690c5c9 100644 --- a/Sources/App/Core/Extensions/Result+ext.swift +++ b/Sources/App/Core/Extensions/Result+ext.swift @@ -14,14 +14,6 @@ extension Result where Failure == Error { - init(catching body: () async throws -> Success) async { - do { - self = .success(try await body()) - } catch { - self = .failure(error) - } - } - var isSucess: Bool { switch self { case .success: diff --git a/Sources/App/Core/PackageCollection+generate.swift b/Sources/App/Core/PackageCollection+generate.swift index 676cd2708..a26d1a45c 100644 --- a/Sources/App/Core/PackageCollection+generate.swift +++ b/Sources/App/Core/PackageCollection+generate.swift @@ -12,8 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Fluent import Foundation + +import Dependencies +import Fluent import PackageCollectionsModel @@ -58,6 +60,8 @@ extension PackageCollection { guard !packages.isEmpty else { throw Error.noResults } + @Dependency(\.date.now) var now + return PackageCollection.init( name: collectionName, overview: overview, @@ -65,7 +69,7 @@ extension PackageCollection { packages: packages, formatVersion: .v1_0, revision: revision, - generatedAt: Current.date(), + generatedAt: now, generatedBy: authorName.map(Author.init(name:)) ) } diff --git a/Sources/App/Core/Score.swift b/Sources/App/Core/Score.swift index e6f83a209..04c2be308 100644 --- a/Sources/App/Core/Score.swift +++ b/Sources/App/Core/Score.swift @@ -14,6 +14,9 @@ import Foundation +import Dependencies + + enum Score { struct Details: Codable, Equatable { var licenseKind: License.Kind @@ -99,7 +102,8 @@ enum Score { // Last maintenance activity // Note: This is not the most accurate method to calculate the number of days between // two dates, but is more than good enough for the purposes of this calculation. - let dateDifference = Calendar.current.dateComponents([.day], from: candidate.lastActivityAt, to: Current.date()) + @Dependency(\.date.now) var now + let dateDifference = Calendar.current.dateComponents([.day], from: candidate.lastActivityAt, to: now) switch dateDifference.day { case .some(..<30): scoreBreakdown[.maintenance] = 15 diff --git a/Sources/App/Models/Package.swift b/Sources/App/Models/Package.swift index 809257b93..387dddcf0 100644 --- a/Sources/App/Models/Package.swift +++ b/Sources/App/Models/Package.swift @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Dependencies import Fluent +import SQLKit import SemanticVersion import Vapor -import SQLKit final class Package: @unchecked Sendable, Model, Content { @@ -310,6 +311,7 @@ extension Package { private extension JoinedQueryBuilder where J == Joined { func filter(for stage: Package.ProcessingStage) -> Self { + @Dependency(\.date.now) var now switch stage { case .reconciliation: fatalError("reconciliation stage does not select candidates") @@ -320,7 +322,7 @@ private extension JoinedQueryBuilder where J == Joined { .group(.and) { $0 .filter(\.$processingStage == .analysis) - .filter(\.$updatedAt < Current.date().addingTimeInterval(-Constants.reIngestionDeadtime) + .filter(\.$updatedAt < now.addingTimeInterval(-Constants.reIngestionDeadtime) ) } } diff --git a/Sources/App/Views/BuildMonitorController/BuildMonitorIndex+Model.swift b/Sources/App/Views/BuildMonitorController/BuildMonitorIndex+Model.swift index 8f0a35ec9..195bc4df1 100644 --- a/Sources/App/Views/BuildMonitorController/BuildMonitorIndex+Model.swift +++ b/Sources/App/Views/BuildMonitorController/BuildMonitorIndex+Model.swift @@ -13,8 +13,11 @@ // limitations under the License. import Foundation + +import Dependencies import Plot + extension BuildMonitorIndex { struct Model { @@ -65,7 +68,8 @@ extension BuildMonitorIndex { } func buildMonitorItem() -> Node { - .a( + @Dependency(\.date.now) var now + return .a( .href(SiteURL.builds(.value(buildId)).relativeURL()), .div( .class("row"), @@ -82,7 +86,7 @@ extension BuildMonitorIndex { .text(status.description) ), .small( - .text("\(date: createdAt, relativeTo: Current.date())") + .text("\(date: createdAt, relativeTo: now)") ) ), .div( diff --git a/Sources/App/Views/Home/HomeIndex+Query.swift b/Sources/App/Views/Home/HomeIndex+Query.swift index 893413407..62f8a27da 100644 --- a/Sources/App/Views/Home/HomeIndex+Query.swift +++ b/Sources/App/Views/Home/HomeIndex+Query.swift @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Dependencies import Fluent import Plot @@ -46,7 +47,7 @@ extension HomeIndex.Model.Release { init(recent: RecentRelease) { packageName = recent.packageName version = recent.version - date = "\(date: recent.releasedAt, relativeTo: Current.date())" + date = recent.releasedAt.relative url = SiteURL.package(.value(recent.repositoryOwner), .value(recent.repositoryName), .none).relativeURL() diff --git a/Sources/App/Views/PackageController/GetRoute.Model+ext.swift b/Sources/App/Views/PackageController/GetRoute.Model+ext.swift index b647d1696..b794cc6f0 100644 --- a/Sources/App/Views/PackageController/GetRoute.Model+ext.swift +++ b/Sources/App/Views/PackageController/GetRoute.Model+ext.swift @@ -13,10 +13,12 @@ // limitations under the License. import Foundation -import Plot -import Vapor + +import Dependencies import DependencyResolution +import Plot import SPIManifest +import Vapor extension API.PackageController.GetRoute.Model { @@ -181,7 +183,7 @@ extension API.PackageController.GetRoute.Model { return .empty } } - + func forkedListItem() -> Node { if let forkedFromInfo { let item: Node = { @@ -216,7 +218,7 @@ extension API.PackageController.GetRoute.Model { ) } }() - + return .li( .class("forked"), item @@ -272,8 +274,9 @@ extension API.PackageController.GetRoute.Model { commitsLinkNode, " and ", releasesLinkNode, "." ]) } else { + @Dependency(\.date.now) var now releasesSentenceFragments.append(contentsOf: [ - "In development for \(inWords: Current.date().timeIntervalSince(history.createdAt)), with ", + "In development for \(inWords: now.timeIntervalSince(history.createdAt)), with ", commitsLinkNode, " and ", releasesLinkNode, "." ]) diff --git a/Sources/App/Views/PackageController/MaintainerInfo/MaintainerInfoIndex+Model.swift b/Sources/App/Views/PackageController/MaintainerInfo/MaintainerInfoIndex+Model.swift index b17eb8416..cb7978de7 100644 --- a/Sources/App/Views/PackageController/MaintainerInfo/MaintainerInfoIndex+Model.swift +++ b/Sources/App/Views/PackageController/MaintainerInfo/MaintainerInfoIndex+Model.swift @@ -13,6 +13,8 @@ // limitations under the License. import Foundation + +import Dependencies import Plot @@ -30,7 +32,7 @@ extension MaintainerInfoIndex { var score: Int var description: String } - + func badgeURL(for type: BadgeType) -> String { let characterSet = CharacterSet.urlHostAllowed.subtracting(.init(charactersIn: "=:")) let url = SiteURL.api(.packages(.value(repositoryOwner), .value(repositoryName), .badge)).absoluteURL(parameters: [QueryParameter(key: "type", value: type.rawValue)]) @@ -48,7 +50,7 @@ extension MaintainerInfoIndex { eventName: "Copy Markdown Button", valueToCopy: badgeMarkdown(for: type)) } - + func packageScoreCategories() -> Node { .forEach(0.. Calendar.current.date(byAdding: .init(day: -750), - to: Current.date()) ?? Current.date() + to: now) ?? now return maintainedRecently ? "Last maintenance activity \(lastActivityAt.relative)." : "No recent maintenance activity." diff --git a/Sources/App/Views/PackageController/PackageReleases+Model.swift b/Sources/App/Views/PackageController/PackageReleases+Model.swift index 1458c90a5..868381da3 100644 --- a/Sources/App/Views/PackageController/PackageReleases+Model.swift +++ b/Sources/App/Views/PackageController/PackageReleases+Model.swift @@ -13,8 +13,11 @@ // limitations under the License. import Foundation -import Vapor + +import Dependencies import SwiftSoup +import Vapor + extension PackageReleases { @@ -55,9 +58,10 @@ extension PackageReleases { self.releases = releases } - static func formatDate(_ date: Date?, currentDate: Date = Current.date()) -> String? { + static func formatDate(_ date: Date?, currentDate: Date? = nil) -> String? { guard let date = date else { return nil } - return "Released \(date: date, relativeTo: currentDate) on \(Self.dateFormatter.string(from: date))" + @Dependency(\.date.now) var now + return "Released \(date: date, relativeTo: currentDate ?? now) on \(Self.dateFormatter.string(from: date))" } static func updateDescription(_ description: String?, replacingTitle title: String) -> String? { diff --git a/Sources/App/Views/Plot+Extensions.swift b/Sources/App/Views/Plot+Extensions.swift index 85808b357..a877d0fbc 100644 --- a/Sources/App/Views/Plot+Extensions.swift +++ b/Sources/App/Views/Plot+Extensions.swift @@ -13,8 +13,11 @@ // limitations under the License. import Foundation + +import Dependencies import Plot + extension Node where Context: HTML.BodyContext { static func turboFrame(id: String, source: String? = nil, _ nodes: Node...) -> Self { let attributes: [Node] = [ @@ -271,7 +274,8 @@ extension Node where Context == HTML.ListContext { stars: Int?, lastActivityAt: Date?, hasDocs: Bool) -> Self { - .li( + @Dependency(\.date.now) var now + return .li( .a( .href(linkUrl), .h4(.text(packageName)), @@ -309,7 +313,7 @@ extension Node where Context == HTML.ListContext { .li( .class("activity"), .small( - .text("Active \(date: $0, relativeTo: Current.date())") + .text("Active \(date: $0, relativeTo: now)") ) ) }, diff --git a/Sources/App/Views/ResourceReloadIdentifier.swift b/Sources/App/Views/ResourceReloadIdentifier.swift index dffac9b09..2be4c37ef 100644 --- a/Sources/App/Views/ResourceReloadIdentifier.swift +++ b/Sources/App/Views/ResourceReloadIdentifier.swift @@ -13,8 +13,11 @@ // limitations under the License. import Foundation + +import Dependencies import Vapor + struct ResourceReloadIdentifier { static var value: String { // In staging or production appVersion will be set to a commit hash or a tag name. @@ -31,15 +34,16 @@ struct ResourceReloadIdentifier { } private static func modificationDate(forLocalResource resource: String) -> Date { + @Dependency(\.date.now) var now let pathToPublic = DirectoryConfiguration.detect().publicDirectory let url = URL(fileURLWithPath: pathToPublic + resource) // Assume the file has been modified *now* if the file can't be found. guard let attributes = try? Current.fileManager.attributesOfItem(atPath: url.path) - else { return Current.date() } + else { return now } // Also assume the file is modified now if the attribute doesn't exist. let modificationDate = attributes[FileAttributeKey.modificationDate] as? Date - return modificationDate ?? Current.date() + return modificationDate ?? now } } diff --git a/Tests/AppTests/API+PackageController+GetRoute+ModelTests.swift b/Tests/AppTests/API+PackageController+GetRoute+ModelTests.swift index f5d242a31..bd8737c10 100644 --- a/Tests/AppTests/API+PackageController+GetRoute+ModelTests.swift +++ b/Tests/AppTests/API+PackageController+GetRoute+ModelTests.swift @@ -166,7 +166,7 @@ class API_PackageController_GetRoute_ModelTests: SnapshotTestCase { func test_history() throws { var model = API.PackageController.GetRoute.Model.mock model.history = .init( - createdAt: Calendar.current.date(byAdding: .month, value: -7, to: Current.date())!, + createdAt: Calendar.current.date(byAdding: .month, value: -7, to: .t0)!, commitCount: 12, commitCountURL: "https://example.com/commits.html", releaseCount: 2, @@ -229,7 +229,7 @@ class API_PackageController_GetRoute_ModelTests: SnapshotTestCase { func test_history_archived_package() throws { var model = API.PackageController.GetRoute.Model.mock model.history = .init( - createdAt: Calendar.current.date(byAdding: .month, value: -7, to: Current.date())!, + createdAt: Calendar.current.date(byAdding: .month, value: -7, to: Date.now)!, commitCount: 12, commitCountURL: "https://example.com/commits.html", releaseCount: 2, @@ -560,20 +560,20 @@ class API_PackageController_GetRoute_ModelTests: SnapshotTestCase { try await [ try App.Version(package: pkg, reference: .branch("branch")), try App.Version(package: pkg, - commitDate: Current.date().adding(days: -1), + commitDate: Date.now.adding(days: -1), latest: .defaultBranch, reference: .branch("default"), supportedPlatforms: [.macos("10.15"), .ios("13")], swiftVersions: ["5.2", "5.3"].asSwiftVersions), try App.Version(package: pkg, reference: .tag(.init(1, 2, 3))), try App.Version(package: pkg, - commitDate: Current.date().adding(days: -3), + commitDate: Date.now.adding(days: -3), latest: .release, reference: .tag(.init(2, 1, 0)), supportedPlatforms: [.macos("10.13"), .ios("10")], swiftVersions: ["4", "5"].asSwiftVersions), try App.Version(package: pkg, - commitDate: Current.date().adding(days: -2), + commitDate: Date.now.adding(days: -2), latest: .preRelease, reference: .tag(.init(3, 0, 0, "beta")), supportedPlatforms: [.macos("10.14"), .ios("13")], diff --git a/Tests/AppTests/API+PackageControllerTests.swift b/Tests/AppTests/API+PackageControllerTests.swift index 4633f1de2..6cda66eee 100644 --- a/Tests/AppTests/API+PackageControllerTests.swift +++ b/Tests/AppTests/API+PackageControllerTests.swift @@ -12,10 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +import XCTest + @testable import App +import Dependencies import Vapor -import XCTest class API_PackageControllerTests: AppTestCase { @@ -23,68 +25,68 @@ class API_PackageControllerTests: AppTestCase { typealias BuildDetails = (reference: Reference, platform: Build.Platform, swiftVersion: SwiftVersion, status: Build.Status) func test_History_query() async throws { - // setup - Current.date = { - Date.init(timeIntervalSince1970: 1608000588) // Dec 15, 2020 - } - let pkg = try await savePackage(on: app.db, "1") - try await Repository(package: pkg, - commitCount: 1433, - defaultBranch: "default", - firstCommitDate: .t0, - name: "bar", - owner: "foo").create(on: app.db) - for idx in (0..<10) { - try await Version(package: pkg, - latest: .defaultBranch, - reference: .branch("main")).create(on: app.db) - try await Version(package: pkg, - latest: .release, - reference: .tag(.init(idx, 0, 0))).create(on: app.db) - } - // add pre-release and default branch - these should *not* be counted as releases - try await Version(package: pkg, reference: .branch("main")).create(on: app.db) - try await Version(package: pkg, reference: .tag(.init(2, 0, 0, "beta2"), "2.0.0beta2")).create(on: app.db) + try await withDependencies { + $0.date.now = .init(timeIntervalSince1970: 1608000588) // Dec 15, 2020 + } operation: { + let pkg = try await savePackage(on: app.db, "1") + try await Repository(package: pkg, + commitCount: 1433, + defaultBranch: "default", + firstCommitDate: .t0, + name: "bar", + owner: "foo").create(on: app.db) + for idx in (0..<10) { + try await Version(package: pkg, + latest: .defaultBranch, + reference: .branch("main")).create(on: app.db) + try await Version(package: pkg, + latest: .release, + reference: .tag(.init(idx, 0, 0))).create(on: app.db) + } + // add pre-release and default branch - these should *not* be counted as releases + try await Version(package: pkg, reference: .branch("main")).create(on: app.db) + try await Version(package: pkg, reference: .tag(.init(2, 0, 0, "beta2"), "2.0.0beta2")).create(on: app.db) - // MUT - let record = try await API.PackageController.History.query(on: app.db, owner: "foo", repository: "bar").unwrap() + // MUT + let record = try await API.PackageController.History.query(on: app.db, owner: "foo", repository: "bar").unwrap() - // validate - XCTAssertEqual( - record, - .init(url: "1", - defaultBranch: "default", - firstCommitDate: .t0, - commitCount: 1433, - releaseCount: 10) - ) + // validate + XCTAssertEqual( + record, + .init(url: "1", + defaultBranch: "default", + firstCommitDate: .t0, + commitCount: 1433, + releaseCount: 10) + ) + } } func test_History_query_no_releases() async throws { - // setup - Current.date = { - Date.init(timeIntervalSince1970: 1608000588) // Dec 15, 2020 + try await withDependencies { + $0.date.now = .init(timeIntervalSince1970: 1608000588) // Dec 15, 2020 + } operation: { + let pkg = try await savePackage(on: app.db, "1") + try await Repository(package: pkg, + commitCount: 1433, + defaultBranch: "default", + firstCommitDate: .t0, + name: "bar", + owner: "foo").create(on: app.db) + + // MUT + let record = try await API.PackageController.History.query(on: app.db, owner: "foo", repository: "bar").unwrap() + + // validate + XCTAssertEqual( + record, + .init(url: "1", + defaultBranch: "default", + firstCommitDate: .t0, + commitCount: 1433, + releaseCount: 0) + ) } - let pkg = try await savePackage(on: app.db, "1") - try await Repository(package: pkg, - commitCount: 1433, - defaultBranch: "default", - firstCommitDate: .t0, - name: "bar", - owner: "foo").create(on: app.db) - - // MUT - let record = try await API.PackageController.History.query(on: app.db, owner: "foo", repository: "bar").unwrap() - - // validate - XCTAssertEqual( - record, - .init(url: "1", - defaultBranch: "default", - firstCommitDate: .t0, - commitCount: 1433, - releaseCount: 0) - ) } func test_History_Record_historyModel() throws { diff --git a/Tests/AppTests/AnalyzeErrorTests.swift b/Tests/AppTests/AnalyzeErrorTests.swift index 720254751..8ee2a775f 100644 --- a/Tests/AppTests/AnalyzeErrorTests.swift +++ b/Tests/AppTests/AnalyzeErrorTests.swift @@ -16,6 +16,7 @@ import XCTest @testable import App +import Dependencies import Fluent import ShellOut @@ -32,7 +33,7 @@ final class AnalyzeErrorTests: AppTestCase { let badPackageID: Package.Id = .id0 let goodPackageID: Package.Id = .id1 - let socialPosts = ActorIsolated<[String]>([]) + let socialPosts = LockIsolated<[String]>([]) static let defaultShellRun: @Sendable (ShellOutCommand, String) throws -> String = { @Sendable cmd, path in switch cmd { @@ -53,7 +54,7 @@ final class AnalyzeErrorTests: AppTestCase { override func setUp() async throws { try await super.setUp() - await socialPosts.setValue([]) + socialPosts.setValue([]) let pkgs = [ Package(id: badPackageID, @@ -99,112 +100,126 @@ final class AnalyzeErrorTests: AppTestCase { Current.shell.run = Self.defaultShellRun Current.mastodonPost = { [socialPosts = self.socialPosts] _, message in - await socialPosts.withValue { $0.append(message) } + socialPosts.withValue { $0.append(message) } } } func test_analyze_refreshCheckout_failed() async throws { - // setup - Current.shell.run = { @Sendable cmd, path in - switch cmd { - case _ where cmd.description.contains("git clone https://github.com/foo/1"): - throw SimulatedError() - - case .gitFetchAndPruneTags where path.hasSuffix("foo-1"): - throw SimulatedError() - - default: - return try Self.defaultShellRun(cmd, path) + try await withDependencies { + $0.date.now = .t0 + } operation: { + Current.shell.run = { @Sendable cmd, path in + switch cmd { + case _ where cmd.description.contains("git clone https://github.com/foo/1"): + throw SimulatedError() + + case .gitFetchAndPruneTags where path.hasSuffix("foo-1"): + throw SimulatedError() + + default: + return try Self.defaultShellRun(cmd, path) + } + } + + // MUT + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) + + // validate + try await defaultValidation() + try logger.logs.withValue { logs in + XCTAssertEqual(logs.count, 2) + let error = try logs.last.unwrap() + XCTAssertTrue(error.message.contains("refreshCheckout failed"), "was: \(error.message)") } - } - - // MUT - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) - - // validate - try await defaultValidation() - try logger.logs.withValue { logs in - XCTAssertEqual(logs.count, 2) - let error = try logs.last.unwrap() - XCTAssertTrue(error.message.contains("refreshCheckout failed"), "was: \(error.message)") } } func test_analyze_updateRepository_invalidPackageCachePath() async throws { - // setup - let pkg = try await Package.find(badPackageID, on: app.db).unwrap() - // This may look weird but its currently the only way to actually create an - // invalid package cache path - we need to mess up the package url. - pkg.url = "foo/1" - XCTAssertNil(pkg.cacheDirectoryName) - try await pkg.save(on: app.db) - - // MUT - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) - - // validate - try await defaultValidation() - try logger.logs.withValue { logs in - XCTAssertEqual(logs.count, 2) - let error = try logs.last.unwrap() - XCTAssertTrue(error.message.contains( "AppError.invalidPackageCachePath"), "was: \(error.message)") + try await withDependencies { + $0.date.now = .t0 + } operation: { + // setup + let pkg = try await Package.find(badPackageID, on: app.db).unwrap() + // This may look weird but its currently the only way to actually create an + // invalid package cache path - we need to mess up the package url. + pkg.url = "foo/1" + XCTAssertNil(pkg.cacheDirectoryName) + try await pkg.save(on: app.db) + + // MUT + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) + + // validate + try await defaultValidation() + try logger.logs.withValue { logs in + XCTAssertEqual(logs.count, 2) + let error = try logs.last.unwrap() + XCTAssertTrue(error.message.contains( "AppError.invalidPackageCachePath"), "was: \(error.message)") + } } } func test_analyze_getPackageInfo_gitCheckout_error() async throws { - // setup - Current.shell.run = { @Sendable cmd, path in - switch cmd { - case .gitCheckout(branch: "main", quiet: true) where path.hasSuffix("foo-1"): - throw SimulatedError() - - default: - return try Self.defaultShellRun(cmd, path) + try await withDependencies { + $0.date.now = .t0 + } operation: { + // setup + Current.shell.run = { @Sendable cmd, path in + switch cmd { + case .gitCheckout(branch: "main", quiet: true) where path.hasSuffix("foo-1"): + throw SimulatedError() + + default: + return try Self.defaultShellRun(cmd, path) + } } - } - // MUT - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) - - // validate - try await defaultValidation() - try logger.logs.withValue { logs in - XCTAssertEqual(logs.count, 2) - let error = try logs.last.unwrap() - XCTAssertTrue(error.message.contains("AppError.noValidVersions"), "was: \(error.message)") + // MUT + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) + + // validate + try await defaultValidation() + try logger.logs.withValue { logs in + XCTAssertEqual(logs.count, 2) + let error = try logs.last.unwrap() + XCTAssertTrue(error.message.contains("AppError.noValidVersions"), "was: \(error.message)") + } } } func test_analyze_dumpPackage_missing_manifest() async throws { - // setup - Current.fileManager.fileExists = { @Sendable path in - if path.hasSuffix("github.com-foo-1/Package.swift") { - return false + try await withDependencies { + $0.date.now = .t0 + } operation: { + // setup + Current.fileManager.fileExists = { @Sendable path in + if path.hasSuffix("github.com-foo-1/Package.swift") { + return false + } + return true + } + + // MUT + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) + + // validate + try await defaultValidation() + try logger.logs.withValue { logs in + XCTAssertEqual(logs.count, 2) + let error = try logs.last.unwrap() + XCTAssertTrue(error.message.contains("AppError.noValidVersions"), "was: \(error.message)") } - return true - } - - // MUT - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) - - // validate - try await defaultValidation() - try logger.logs.withValue { logs in - XCTAssertEqual(logs.count, 2) - let error = try logs.last.unwrap() - XCTAssertTrue(error.message.contains("AppError.noValidVersions"), "was: \(error.message)") } } - } @@ -216,7 +231,7 @@ extension AnalyzeErrorTests { XCTAssertEqual(versions.count, 2) XCTAssertEqual(versions.filter(\.isBranch).first?.latest, .defaultBranch) XCTAssertEqual(versions.filter(\.isTag).first?.latest, .release) - await socialPosts.withValue { tweets in + socialPosts.withValue { tweets in XCTAssertEqual(tweets, [ """ ⬆️ foo just released foo-2 v1.2.3 diff --git a/Tests/AppTests/AnalyzerTests.swift b/Tests/AppTests/AnalyzerTests.swift index f03806aa0..60f75f0cb 100644 --- a/Tests/AppTests/AnalyzerTests.swift +++ b/Tests/AppTests/AnalyzerTests.swift @@ -16,12 +16,14 @@ import XCTest @testable import App +import Dependencies import Fluent +import NIOConcurrencyHelpers import SPIManifest -@preconcurrency import ShellOut import SnapshotTesting import Vapor -import NIOConcurrencyHelpers + +@preconcurrency import ShellOut class AnalyzerTests: AppTestCase { @@ -31,61 +33,64 @@ class AnalyzerTests: AppTestCase { // End-to-end test, where we mock at the shell command level (i.e. we // don't mock the git commands themselves to ensure we're running the // expected shell commands for the happy path.) - // setup - let urls = ["https://github.com/foo/1", "https://github.com/foo/2"] - let pkgs = try await savePackages(on: app.db, urls.asURLs, processingStage: .ingestion) - try await Repository(package: pkgs[0], - defaultBranch: "main", - name: "1", - owner: "foo", - releases: [ - .mock(description: "rel 1.0.0", tagName: "1.0.0") - ], - stars: 25).save(on: app.db) - try await Repository(package: pkgs[1], - defaultBranch: "main", - name: "2", - owner: "foo", - stars: 100).save(on: app.db) - - let checkoutDir = QueueIsolated(nil) - let commands = QueueIsolated<[Command]>([]) - let firstDirCloned = QueueIsolated(false) - Current.fileManager.fileExists = { @Sendable path in - if let outDir = checkoutDir.value, - path == "\(outDir)/github.com-foo-1" { return firstDirCloned.value } - // let the check for the second repo checkout path succeed to simulate pull - if let outDir = checkoutDir.value, - path == "\(outDir)/github.com-foo-2" { return true } - if path.hasSuffix("Package.swift") { return true } - if path.hasSuffix("Package.resolved") { return true } - return false - } - Current.fileManager.createDirectory = { @Sendable path, _, _ in checkoutDir.setValue(path) } - Current.git = .live - Current.loadSPIManifest = { path in - if path.hasSuffix("foo-1") { - return .init(builder: .init(configs: [.init(documentationTargets: ["DocTarget"])])) - } else { - return nil - } - } - Current.shell.run = { @Sendable cmd, path in - let trimmedPath = path.replacingOccurrences(of: checkoutDir.value!, with: ".") - commands.withValue { - $0.append(.init(command: cmd, path: trimmedPath)!) - } - if cmd.description.starts(with: "git clone") { - firstDirCloned.setValue(true) - } - if cmd == .gitListTags && path.hasSuffix("foo-1") { - return ["1.0.0", "1.1.1"].joined(separator: "\n") + try await withDependencies { + $0.date.now = .now + } operation: { + // setup + let urls = ["https://github.com/foo/1", "https://github.com/foo/2"] + let pkgs = try await savePackages(on: app.db, urls.asURLs, processingStage: .ingestion) + try await Repository(package: pkgs[0], + defaultBranch: "main", + name: "1", + owner: "foo", + releases: [ + .mock(description: "rel 1.0.0", tagName: "1.0.0") + ], + stars: 25).save(on: app.db) + try await Repository(package: pkgs[1], + defaultBranch: "main", + name: "2", + owner: "foo", + stars: 100).save(on: app.db) + + let checkoutDir = QueueIsolated(nil) + let commands = QueueIsolated<[Command]>([]) + let firstDirCloned = QueueIsolated(false) + Current.fileManager.fileExists = { @Sendable path in + if let outDir = checkoutDir.value, + path == "\(outDir)/github.com-foo-1" { return firstDirCloned.value } + // let the check for the second repo checkout path succeed to simulate pull + if let outDir = checkoutDir.value, + path == "\(outDir)/github.com-foo-2" { return true } + if path.hasSuffix("Package.swift") { return true } + if path.hasSuffix("Package.resolved") { return true } + return false } - if cmd == .gitListTags && path.hasSuffix("foo-2") { - return ["2.0.0", "2.1.0"].joined(separator: "\n") + Current.fileManager.createDirectory = { @Sendable path, _, _ in checkoutDir.setValue(path) } + Current.git = .live + Current.loadSPIManifest = { path in + if path.hasSuffix("foo-1") { + return .init(builder: .init(configs: [.init(documentationTargets: ["DocTarget"])])) + } else { + return nil + } } - if cmd == .swiftDumpPackage && path.hasSuffix("foo-1") { - return #""" + Current.shell.run = { @Sendable cmd, path in + let trimmedPath = path.replacingOccurrences(of: checkoutDir.value!, with: ".") + commands.withValue { + $0.append(.init(command: cmd, path: trimmedPath)!) + } + if cmd.description.starts(with: "git clone") { + firstDirCloned.setValue(true) + } + if cmd == .gitListTags && path.hasSuffix("foo-1") { + return ["1.0.0", "1.1.1"].joined(separator: "\n") + } + if cmd == .gitListTags && path.hasSuffix("foo-2") { + return ["2.0.0", "2.1.0"].joined(separator: "\n") + } + if cmd == .swiftDumpPackage && path.hasSuffix("foo-1") { + return #""" { "name": "foo-1", "products": [ @@ -100,9 +105,9 @@ class AnalyzerTests: AppTestCase { "targets": [{"name": "t1", "type": "executable"}] } """# - } - if cmd == .swiftDumpPackage && path.hasSuffix("foo-2") { - return #""" + } + if cmd == .swiftDumpPackage && path.hasSuffix("foo-2") { + return #""" { "name": "foo-2", "products": [ @@ -117,152 +122,156 @@ class AnalyzerTests: AppTestCase { "targets": [{"name": "t2", "type": "regular"}] } """# - } + } - // Git.revisionInfo (per ref - default branch & tags) - // These return a string in the format `commit sha`-`timestamp (sec since 1970)` - // We simply use `sha` for the sha (it bears no meaning) and a range of seconds - // since 1970. - // It is important the tags aren't created at identical times for tags on the same - // package, or else we will collect multiple recent releases (as there is no "latest") - if cmd == .gitRevisionInfo(reference: .tag(1, 0, 0)) { return "sha-0" } - if cmd == .gitRevisionInfo(reference: .tag(1, 1, 1)) { return "sha-1" } - if cmd == .gitRevisionInfo(reference: .tag(2, 0, 0)) { return "sha-2" } - if cmd == .gitRevisionInfo(reference: .tag(2, 1, 0)) { return "sha-3" } - if cmd == .gitRevisionInfo(reference: .branch("main")) { return "sha-4" } - - if cmd == .gitCommitCount { return "12" } - if cmd == .gitFirstCommitDate { return "0" } - if cmd == .gitLastCommitDate { return "4" } - if cmd == .gitShortlog { - return "10\tPerson 1" - } + // Git.revisionInfo (per ref - default branch & tags) + // These return a string in the format `commit sha`-`timestamp (sec since 1970)` + // We simply use `sha` for the sha (it bears no meaning) and a range of seconds + // since 1970. + // It is important the tags aren't created at identical times for tags on the same + // package, or else we will collect multiple recent releases (as there is no "latest") + if cmd == .gitRevisionInfo(reference: .tag(1, 0, 0)) { return "sha-0" } + if cmd == .gitRevisionInfo(reference: .tag(1, 1, 1)) { return "sha-1" } + if cmd == .gitRevisionInfo(reference: .tag(2, 0, 0)) { return "sha-2" } + if cmd == .gitRevisionInfo(reference: .tag(2, 1, 0)) { return "sha-3" } + if cmd == .gitRevisionInfo(reference: .branch("main")) { return "sha-4" } + + if cmd == .gitCommitCount { return "12" } + if cmd == .gitFirstCommitDate { return "0" } + if cmd == .gitLastCommitDate { return "4" } + if cmd == .gitShortlog { + return "10\tPerson 1" + } - return "" - } + return "" + } - // MUT - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) + // MUT + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) + + // validation + let outDir = try checkoutDir.value.unwrap() + XCTAssert(outDir.hasSuffix("SPI-checkouts"), "unexpected checkout dir, was: \(outDir)") + XCTAssertEqual(commands.value.count, 36) + + // Snapshot for each package individually to avoid ordering issues when + // concurrent processing causes commands to interleave between packages. + assertSnapshot(of: commands.value + .filter { $0.path.hasSuffix("foo-1") } + .map(\.description), as: .dump) + assertSnapshot(of: commands.value + .filter { $0.path.hasSuffix("foo-2") } + .map(\.description), as: .dump) - // validation - let outDir = try checkoutDir.value.unwrap() - XCTAssert(outDir.hasSuffix("SPI-checkouts"), "unexpected checkout dir, was: \(outDir)") - XCTAssertEqual(commands.value.count, 36) - - // Snapshot for each package individually to avoid ordering issues when - // concurrent processing causes commands to interleave between packages. - assertSnapshot(of: commands.value - .filter { $0.path.hasSuffix("foo-1") } - .map(\.description), as: .dump) - assertSnapshot(of: commands.value - .filter { $0.path.hasSuffix("foo-2") } - .map(\.description), as: .dump) - - // validate versions - // A bit awkward... create a helper? There has to be a better way? - let pkg1 = try await Package.query(on: app.db).filter(by: urls[0].url).with(\.$versions).first()! - XCTAssertEqual(pkg1.status, .ok) - XCTAssertEqual(pkg1.processingStage, .analysis) - XCTAssertEqual(pkg1.versions.map(\.packageName), ["foo-1", "foo-1", "foo-1"]) - let sortedVersions1 = pkg1.versions.sorted(by: { $0.createdAt! < $1.createdAt! }) - XCTAssertEqual(sortedVersions1.map(\.reference.description), ["main", "1.0.0", "1.1.1"]) - XCTAssertEqual(sortedVersions1.map(\.latest), [.defaultBranch, nil, .release]) - XCTAssertEqual(sortedVersions1.map(\.releaseNotes), [nil, "rel 1.0.0", nil]) - - let pkg2 = try await Package.query(on: app.db).filter(by: urls[1].url).with(\.$versions).first()! - XCTAssertEqual(pkg2.status, .ok) - XCTAssertEqual(pkg2.processingStage, .analysis) - XCTAssertEqual(pkg2.versions.map(\.packageName), ["foo-2", "foo-2", "foo-2"]) - let sortedVersions2 = pkg2.versions.sorted(by: { $0.createdAt! < $1.createdAt! }) - XCTAssertEqual(sortedVersions2.map(\.reference.description), ["main", "2.0.0", "2.1.0"]) - XCTAssertEqual(sortedVersions2.map(\.latest), [.defaultBranch, nil, .release]) - - // validate products - // (2 packages with 3 versions with 1 product each = 6 products) - let products = try await Product.query(on: app.db).sort(\.$name).all() - XCTAssertEqual(products.count, 6) - assertEquals(products, \.name, ["p1", "p1", "p1", "p2", "p2", "p2"]) - assertEquals(products, \.targets, - [["t1"], ["t1"], ["t1"], ["t2"], ["t2"], ["t2"]]) - assertEquals(products, \.type, [.executable, .executable, .executable, .library(.automatic), .library(.automatic), .library(.automatic)]) - - // validate targets - // (2 packages with 3 versions with 1 target each = 6 targets) - let targets = try await Target.query(on: app.db).sort(\.$name).all() - XCTAssertEqual(targets.map(\.name), ["t1", "t1", "t1", "t2", "t2", "t2"]) - - // validate score - XCTAssertEqual(pkg1.score, 30) - XCTAssertEqual(pkg2.score, 40) - - // ensure stats, recent packages, and releases are refreshed - let app = self.app! - try await XCTAssertEqualAsync(try await Stats.fetch(on: app.db), .init(packageCount: 2)) - try await XCTAssertEqualAsync(try await RecentPackage.fetch(on: app.db).count, 2) - try await XCTAssertEqualAsync(try await RecentRelease.fetch(on: app.db).count, 2) + // validate versions + // A bit awkward... create a helper? There has to be a better way? + let pkg1 = try await Package.query(on: app.db).filter(by: urls[0].url).with(\.$versions).first()! + XCTAssertEqual(pkg1.status, .ok) + XCTAssertEqual(pkg1.processingStage, .analysis) + XCTAssertEqual(pkg1.versions.map(\.packageName), ["foo-1", "foo-1", "foo-1"]) + let sortedVersions1 = pkg1.versions.sorted(by: { $0.createdAt! < $1.createdAt! }) + XCTAssertEqual(sortedVersions1.map(\.reference.description), ["main", "1.0.0", "1.1.1"]) + XCTAssertEqual(sortedVersions1.map(\.latest), [.defaultBranch, nil, .release]) + XCTAssertEqual(sortedVersions1.map(\.releaseNotes), [nil, "rel 1.0.0", nil]) + + let pkg2 = try await Package.query(on: app.db).filter(by: urls[1].url).with(\.$versions).first()! + XCTAssertEqual(pkg2.status, .ok) + XCTAssertEqual(pkg2.processingStage, .analysis) + XCTAssertEqual(pkg2.versions.map(\.packageName), ["foo-2", "foo-2", "foo-2"]) + let sortedVersions2 = pkg2.versions.sorted(by: { $0.createdAt! < $1.createdAt! }) + XCTAssertEqual(sortedVersions2.map(\.reference.description), ["main", "2.0.0", "2.1.0"]) + XCTAssertEqual(sortedVersions2.map(\.latest), [.defaultBranch, nil, .release]) + + // validate products + // (2 packages with 3 versions with 1 product each = 6 products) + let products = try await Product.query(on: app.db).sort(\.$name).all() + XCTAssertEqual(products.count, 6) + assertEquals(products, \.name, ["p1", "p1", "p1", "p2", "p2", "p2"]) + assertEquals(products, \.targets, + [["t1"], ["t1"], ["t1"], ["t2"], ["t2"], ["t2"]]) + assertEquals(products, \.type, [.executable, .executable, .executable, .library(.automatic), .library(.automatic), .library(.automatic)]) + + // validate targets + // (2 packages with 3 versions with 1 target each = 6 targets) + let targets = try await Target.query(on: app.db).sort(\.$name).all() + XCTAssertEqual(targets.map(\.name), ["t1", "t1", "t1", "t2", "t2", "t2"]) + + // validate score + XCTAssertEqual(pkg1.score, 30) + XCTAssertEqual(pkg2.score, 40) + + // ensure stats, recent packages, and releases are refreshed + let app = self.app! + try await XCTAssertEqualAsync(try await Stats.fetch(on: app.db), .init(packageCount: 2)) + try await XCTAssertEqualAsync(try await RecentPackage.fetch(on: app.db).count, 2) + try await XCTAssertEqualAsync(try await RecentRelease.fetch(on: app.db).count, 2) + } } func test_analyze_version_update() async throws { // Ensure that new incoming versions update the latest properties and // move versions in case commits change. Tests both default branch commits // changing as well as a tag being moved to a different commit. - // setup - let pkgId = UUID() - let pkg = Package(id: pkgId, url: "1".asGithubUrl.url, processingStage: .ingestion) - try await pkg.save(on: app.db) - try await Repository(package: pkg, - defaultBranch: "main", - name: "1", - owner: "foo").save(on: app.db) - // add existing versions (to be reconciled) - try await Version(package: pkg, - commit: "commit0", - commitDate: .t0, - latest: .defaultBranch, - packageName: "foo-1", - reference: .branch("main")).save(on: app.db) - try await Version(package: pkg, - commit: "commit0", - commitDate: .t0, - latest: .release, - packageName: "foo-1", - reference: .tag(1, 0, 0)).save(on: app.db) - - Current.fileManager.fileExists = { @Sendable _ in true } - - Current.git.commitCount = { @Sendable _ in 12 } - Current.git.firstCommitDate = { @Sendable _ in .t0 } - Current.git.lastCommitDate = { @Sendable _ in .t2 } - Current.git.getTags = { @Sendable _ in [.tag(1, 0, 0), .tag(1, 1, 1)] } - Current.git.hasBranch = { @Sendable _, _ in true } - Current.git.revisionInfo = { @Sendable ref, _ in - // simulate the following scenario: - // - main branch has moved from commit0 -> commit3 (timestamp t3) - // - 1.0.0 has been re-tagged (!) from commit0 -> commit1 (timestamp t1) - // - 1.1.1 has been added at commit2 (timestamp t2) - switch ref { - case _ where ref == .tag(1, 0, 0): - return .init(commit: "commit1", date: .t1) - case _ where ref == .tag(1, 1, 1): - return .init(commit: "commit2", date: .t2) - case .branch("main"): - return .init(commit: "commit3", date: .t3) - default: - fatalError("unexpected reference: \(ref)") + try await withDependencies { + $0.date.now = .now + } operation: { + // setup + let pkgId = UUID() + let pkg = Package(id: pkgId, url: "1".asGithubUrl.url, processingStage: .ingestion) + try await pkg.save(on: app.db) + try await Repository(package: pkg, + defaultBranch: "main", + name: "1", + owner: "foo").save(on: app.db) + // add existing versions (to be reconciled) + try await Version(package: pkg, + commit: "commit0", + commitDate: .t0, + latest: .defaultBranch, + packageName: "foo-1", + reference: .branch("main")).save(on: app.db) + try await Version(package: pkg, + commit: "commit0", + commitDate: .t0, + latest: .release, + packageName: "foo-1", + reference: .tag(1, 0, 0)).save(on: app.db) + + Current.fileManager.fileExists = { @Sendable _ in true } + + Current.git.commitCount = { @Sendable _ in 12 } + Current.git.firstCommitDate = { @Sendable _ in .t0 } + Current.git.lastCommitDate = { @Sendable _ in .t2 } + Current.git.getTags = { @Sendable _ in [.tag(1, 0, 0), .tag(1, 1, 1)] } + Current.git.hasBranch = { @Sendable _, _ in true } + Current.git.revisionInfo = { @Sendable ref, _ in + // simulate the following scenario: + // - main branch has moved from commit0 -> commit3 (timestamp t3) + // - 1.0.0 has been re-tagged (!) from commit0 -> commit1 (timestamp t1) + // - 1.1.1 has been added at commit2 (timestamp t2) + switch ref { + case _ where ref == .tag(1, 0, 0): + return .init(commit: "commit1", date: .t1) + case _ where ref == .tag(1, 1, 1): + return .init(commit: "commit2", date: .t2) + case .branch("main"): + return .init(commit: "commit3", date: .t3) + default: + fatalError("unexpected reference: \(ref)") + } } - } - Current.git.shortlog = { @Sendable _ in + Current.git.shortlog = { @Sendable _ in """ 10\tPerson 1 2\tPerson 2 """ - } + } - Current.shell.run = { @Sendable cmd, path in - if cmd.description.hasSuffix("package dump-package") { - return #""" + Current.shell.run = { @Sendable cmd, path in + if cmd.description.hasSuffix("package dump-package") { + return #""" { "name": "foo-1", "products": [ @@ -277,162 +286,177 @@ class AnalyzerTests: AppTestCase { "targets": [] } """# + } + return "" } - return "" - } - // MUT - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) + // MUT + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) - // validate versions - let p = try await Package.find(pkgId, on: app.db).unwrap() - try await p.$versions.load(on: app.db) - let versions = p.versions.sorted(by: { $0.commitDate < $1.commitDate }) - XCTAssertEqual(versions.map(\.commitDate), [.t1, .t2, .t3]) - XCTAssertEqual(versions.map(\.reference.description), ["1.0.0", "1.1.1", "main"]) - XCTAssertEqual(versions.map(\.latest), [nil, .release, .defaultBranch]) - XCTAssertEqual(versions.map(\.commit), ["commit1", "commit2", "commit3"]) + // validate versions + let p = try await Package.find(pkgId, on: app.db).unwrap() + try await p.$versions.load(on: app.db) + let versions = p.versions.sorted(by: { $0.commitDate < $1.commitDate }) + XCTAssertEqual(versions.map(\.commitDate), [.t1, .t2, .t3]) + XCTAssertEqual(versions.map(\.reference.description), ["1.0.0", "1.1.1", "main"]) + XCTAssertEqual(versions.map(\.latest), [nil, .release, .defaultBranch]) + XCTAssertEqual(versions.map(\.commit), ["commit1", "commit2", "commit3"]) + } } func test_forward_progress_on_analysisError() async throws { // Ensure a package that fails analysis goes back to ingesting and isn't stuck in an analysis loop - // setup - do { - let pkg = try await savePackage(on: app.db, "https://github.com/foo/1", processingStage: .ingestion) - try await Repository(package: pkg, defaultBranch: "main").save(on: app.db) - } - - Current.git.commitCount = { @Sendable _ in 12 } - Current.git.firstCommitDate = { @Sendable _ in .t0 } - Current.git.lastCommitDate = { @Sendable _ in .t1 } - Current.git.hasBranch = { @Sendable _, _ in false } // simulate analysis error via branch mismatch - Current.git.shortlog = { @Sendable _ in "" } - - // Ensure candidate selection is as expected - let app = self.app! - try await XCTAssertEqualAsync( try await Package.fetchCandidates(app.db, for: .ingestion, limit: 10).count, 0) - try await XCTAssertEqualAsync( try await Package.fetchCandidates(app.db, for: .analysis, limit: 10).count, 1) - - // MUT - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) + try await withDependencies { + $0.date.now = .now + } operation: { + // setup + do { + let pkg = try await savePackage(on: app.db, "https://github.com/foo/1", processingStage: .ingestion) + try await Repository(package: pkg, defaultBranch: "main").save(on: app.db) + } - // Ensure candidate selection is now zero for analysis - // (and also for ingestion, as we're immediately after analysis) - try await XCTAssertEqualAsync( try await Package.fetchCandidates(app.db, for: .ingestion, limit: 10).count, 0) - try await XCTAssertEqualAsync( try await Package.fetchCandidates(app.db, for: .analysis, limit: 10).count, 0) + Current.git.commitCount = { @Sendable _ in 12 } + Current.git.firstCommitDate = { @Sendable _ in .t0 } + Current.git.lastCommitDate = { @Sendable _ in .t1 } + Current.git.hasBranch = { @Sendable _, _ in false } // simulate analysis error via branch mismatch + Current.git.shortlog = { @Sendable _ in "" } - // Advance time beyond reIngestionDeadtime - Current.date = { .now.addingTimeInterval(Constants.reIngestionDeadtime) } + // Ensure candidate selection is as expected + let app = self.app! + try await XCTAssertEqualAsync(try await Package.fetchCandidates(app.db, for: .ingestion, limit: 10).count, 0) + try await XCTAssertEqualAsync(try await Package.fetchCandidates(app.db, for: .analysis, limit: 10).count, 1) - // Ensure candidate selection has flipped to ingestion - try await XCTAssertEqualAsync( try await Package.fetchCandidates(app.db, for: .ingestion, limit: 10).count, 1) - try await XCTAssertEqualAsync( try await Package.fetchCandidates(app.db, for: .analysis, limit: 10).count, 0) + // MUT + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) + + // Ensure candidate selection is now zero for analysis + // (and also for ingestion, as we're immediately after analysis) + try await XCTAssertEqualAsync(try await Package.fetchCandidates(app.db, for: .ingestion, limit: 10).count, 0) + try await XCTAssertEqualAsync(try await Package.fetchCandidates(app.db, for: .analysis, limit: 10).count, 0) + + try await withDependencies { + // Advance time beyond reIngestionDeadtime + $0.date.now = .now.addingTimeInterval(Constants.reIngestionDeadtime) + } operation: { + // Ensure candidate selection has flipped to ingestion + let app = self.app! + try await XCTAssertEqualAsync(try await Package.fetchCandidates(app.db, for: .ingestion, limit: 10).count, 1) + try await XCTAssertEqualAsync(try await Package.fetchCandidates(app.db, for: .analysis, limit: 10).count, 0) + } + } } func test_package_status() async throws { // Ensure packages record success/error status - // setup - let urls = ["https://github.com/foo/1", "https://github.com/foo/2"] - let pkgs = try await savePackages(on: app.db, urls.asURLs, processingStage: .ingestion) - for p in pkgs { - try await Repository(package: p, defaultBranch: "main").save(on: app.db) - } - let lastUpdate = Date() + try await withDependencies { + $0.date.now = .now + } operation: { + // setup + let urls = ["https://github.com/foo/1", "https://github.com/foo/2"] + let pkgs = try await savePackages(on: app.db, urls.asURLs, processingStage: .ingestion) + for p in pkgs { + try await Repository(package: p, defaultBranch: "main").save(on: app.db) + } + let lastUpdate = Date() - Current.git.commitCount = { @Sendable _ in 12 } - Current.git.firstCommitDate = { @Sendable _ in .t0 } - Current.git.lastCommitDate = { @Sendable _ in .t1 } - Current.git.getTags = { @Sendable _ in [.tag(1, 0, 0)] } - Current.git.hasBranch = { @Sendable _, _ in true } - Current.git.revisionInfo = { @Sendable _, _ in .init(commit: "sha", date: .t0) } - Current.git.shortlog = { @Sendable _ in + Current.git.commitCount = { @Sendable _ in 12 } + Current.git.firstCommitDate = { @Sendable _ in .t0 } + Current.git.lastCommitDate = { @Sendable _ in .t1 } + Current.git.getTags = { @Sendable _ in [.tag(1, 0, 0)] } + Current.git.hasBranch = { @Sendable _, _ in true } + Current.git.revisionInfo = { @Sendable _, _ in .init(commit: "sha", date: .t0) } + Current.git.shortlog = { @Sendable _ in """ 10\tPerson 1 2\tPerson 2 """ - } - - Current.shell.run = { @Sendable cmd, path in - // first package fails - if cmd.description.hasSuffix("swift package dump-package") && path.hasSuffix("foo-1") { - return "bad data" } - // second package succeeds - if cmd.description.hasSuffix("swift package dump-package") && path.hasSuffix("foo-2") { - return #"{ "name": "SPI-Server", "products": [], "targets": [] }"# + + Current.shell.run = { @Sendable cmd, path in + // first package fails + if cmd.description.hasSuffix("swift package dump-package") && path.hasSuffix("foo-1") { + return "bad data" + } + // second package succeeds + if cmd.description.hasSuffix("swift package dump-package") && path.hasSuffix("foo-2") { + return #"{ "name": "SPI-Server", "products": [], "targets": [] }"# + } + return "" } - return "" - } - // MUT - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) + // MUT + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) - // assert packages have been updated - let packages = try await Package.query(on: app.db).sort(\.$createdAt).all() - packages.forEach { XCTAssert($0.updatedAt! > lastUpdate) } - XCTAssertEqual(packages.map(\.status), [.noValidVersions, .ok]) + // assert packages have been updated + let packages = try await Package.query(on: app.db).sort(\.$createdAt).all() + packages.forEach { XCTAssert($0.updatedAt! > lastUpdate) } + XCTAssertEqual(packages.map(\.status), [.noValidVersions, .ok]) + } } func test_continue_on_exception() async throws { // Test to ensure exceptions don't interrupt processing - // setup - let urls = ["https://github.com/foo/1", "https://github.com/foo/2"] - let pkgs = try await savePackages(on: app.db, urls.asURLs, processingStage: .ingestion) - for p in pkgs { - try await Repository(package: p, defaultBranch: "main").save(on: app.db) - } - let checkoutDir: NIOLockedValueBox = .init(nil) + try await withDependencies { + $0.date.now = .now + } operation: { + // setup + let urls = ["https://github.com/foo/1", "https://github.com/foo/2"] + let pkgs = try await savePackages(on: app.db, urls.asURLs, processingStage: .ingestion) + for p in pkgs { + try await Repository(package: p, defaultBranch: "main").save(on: app.db) + } + let checkoutDir: NIOLockedValueBox = .init(nil) - Current.fileManager.fileExists = { @Sendable path in - if let outDir = checkoutDir.withLockedValue({ $0 }), path == "\(outDir)/github.com-foo-1" { return true } - if let outDir = checkoutDir.withLockedValue({ $0 }), path == "\(outDir)/github.com-foo-2" { return true } - if path.hasSuffix("Package.swift") { return true } - return false - } - Current.fileManager.createDirectory = { @Sendable path, _, _ in checkoutDir.withLockedValue { $0 = path } } - - Current.git = .live - - let refs: [Reference] = [.tag(1, 0, 0), .tag(1, 1, 1), .branch("main")] - let mockResults = { - var res: [ShellOutCommand: String] = [ - .gitListTags: refs.filter(\.isTag).map { "\($0)" }.joined(separator: "\n"), - .gitCommitCount: "12", - .gitFirstCommitDate: "0", - .gitLastCommitDate: "1", - .gitShortlog : """ + Current.fileManager.fileExists = { @Sendable path in + if let outDir = checkoutDir.withLockedValue({ $0 }), path == "\(outDir)/github.com-foo-1" { return true } + if let outDir = checkoutDir.withLockedValue({ $0 }), path == "\(outDir)/github.com-foo-2" { return true } + if path.hasSuffix("Package.swift") { return true } + return false + } + Current.fileManager.createDirectory = { @Sendable path, _, _ in checkoutDir.withLockedValue { $0 = path } } + + Current.git = .live + + let refs: [Reference] = [.tag(1, 0, 0), .tag(1, 1, 1), .branch("main")] + let mockResults = { + var res: [ShellOutCommand: String] = [ + .gitListTags: refs.filter(\.isTag).map { "\($0)" }.joined(separator: "\n"), + .gitCommitCount: "12", + .gitFirstCommitDate: "0", + .gitLastCommitDate: "1", + .gitShortlog : """ 10\tPerson 1 2\tPerson 2 """ - ] - for (idx, ref) in refs.enumerated() { - res[.gitRevisionInfo(reference: ref)] = "sha-\(idx)" - } - return res - }() + ] + for (idx, ref) in refs.enumerated() { + res[.gitRevisionInfo(reference: ref)] = "sha-\(idx)" + } + return res + }() - let commands = QueueIsolated<[Command]>([]) - Current.shell.run = { @Sendable cmd, path in - commands.withValue { - $0.append(.init(command: cmd, path: path)!) - } + let commands = QueueIsolated<[Command]>([]) + Current.shell.run = { @Sendable cmd, path in + commands.withValue { + $0.append(.init(command: cmd, path: path)!) + } - if let result = mockResults[cmd] { return result } + if let result = mockResults[cmd] { return result } - // simulate error in first package - if cmd == .swiftDumpPackage { - if path.hasSuffix("foo-1") { - // Simulate error when reading the manifest - struct Error: Swift.Error { } - throw Error() - } else { - return #""" + // simulate error in first package + if cmd == .swiftDumpPackage { + if path.hasSuffix("foo-1") { + // Simulate error when reading the manifest + struct Error: Swift.Error { } + throw Error() + } else { + return #""" { "name": "foo-2", "products": [ @@ -447,22 +471,23 @@ class AnalyzerTests: AppTestCase { "targets": [{"name": "t1", "type": "executable"}] } """# + } } - } - return "" - } + return "" + } - // MUT - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) + // MUT + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) - // validation (not in detail, this is just to ensure command count is as expected) - XCTAssertEqual(commands.value.count, 40, "was: \(dump(commands.value))") - // 1 packages with 2 tags + 1 default branch each -> 3 versions (the other package fails) - let versionCount = try await Version.query(on: app.db).count() - XCTAssertEqual(versionCount, 3) + // validation (not in detail, this is just to ensure command count is as expected) + XCTAssertEqual(commands.value.count, 40, "was: \(dump(commands.value))") + // 1 packages with 2 tags + 1 default branch each -> 3 versions (the other package fails) + let versionCount = try await Version.query(on: app.db).count() + XCTAssertEqual(versionCount, 3) + } } @MainActor @@ -565,7 +590,7 @@ class AnalyzerTests: AppTestCase { XCTAssertEqual(msg, "Default branch 'main' does not exist in checkout") } } - + func test_getIncomingVersions_no_default_branch() async throws { // setup // saving Package without Repository means it has no default branch @@ -854,22 +879,25 @@ class AnalyzerTests: AppTestCase { func test_issue_29() async throws { // Regression test for issue 29 // https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/29 - // setup - Current.git.commitCount = { @Sendable _ in 12 } - Current.git.firstCommitDate = { @Sendable _ in .t0 } - Current.git.lastCommitDate = { @Sendable _ in .t1 } - Current.git.getTags = { @Sendable _ in [.tag(1, 0, 0), .tag(2, 0, 0)] } - Current.git.hasBranch = { @Sendable _, _ in true } - Current.git.revisionInfo = { @Sendable _, _ in .init(commit: "sha", date: .t0) } - Current.git.shortlog = { @Sendable _ in + try await withDependencies { + $0.date.now = .now + } operation: { + // setup + Current.git.commitCount = { @Sendable _ in 12 } + Current.git.firstCommitDate = { @Sendable _ in .t0 } + Current.git.lastCommitDate = { @Sendable _ in .t1 } + Current.git.getTags = { @Sendable _ in [.tag(1, 0, 0), .tag(2, 0, 0)] } + Current.git.hasBranch = { @Sendable _, _ in true } + Current.git.revisionInfo = { @Sendable _, _ in .init(commit: "sha", date: .t0) } + Current.git.shortlog = { @Sendable _ in """ 10\tPerson 1 2\tPerson 2 """ - } - Current.shell.run = { @Sendable cmd, path in - if cmd.description.hasSuffix("swift package dump-package") { - return #""" + } + Current.shell.run = { @Sendable cmd, path in + if cmd.description.hasSuffix("swift package dump-package") { + return #""" { "name": "foo", "products": [ @@ -891,25 +919,26 @@ class AnalyzerTests: AppTestCase { "targets": [] } """# + } + return "" + } + let pkgs = try await savePackages(on: app.db, ["1", "2"].asGithubUrls.asURLs, processingStage: .ingestion) + for pkg in pkgs { + try await Repository(package: pkg, defaultBranch: "main").save(on: app.db) } - return "" - } - let pkgs = try await savePackages(on: app.db, ["1", "2"].asGithubUrls.asURLs, processingStage: .ingestion) - for pkg in pkgs { - try await Repository(package: pkg, defaultBranch: "main").save(on: app.db) - } - - // MUT - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) - // validation - // 1 version for the default branch + 2 for the tags each = 6 versions - // 2 products per version = 12 products - let db = app.db - try await XCTAssertEqualAsync(try await Version.query(on: db).count(), 6) - try await XCTAssertEqualAsync(try await Product.query(on: db).count(), 12) + // MUT + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) + + // validation + // 1 version for the default branch + 2 for the tags each = 6 versions + // 2 products per version = 12 products + let db = app.db + try await XCTAssertEqualAsync(try await Version.query(on: db).count(), 6) + try await XCTAssertEqualAsync(try await Product.query(on: db).count(), 12) + } } @MainActor @@ -1166,20 +1195,20 @@ class AnalyzerTests: AppTestCase { try await pkg.save(on: app.db) try await Repository(package: pkg, defaultBranch: "main").save(on: app.db) try await Version(package: pkg, - latest: .defaultBranch, - packageName: "foo", - reference: .branch("main")) - .save(on: app.db) + latest: .defaultBranch, + packageName: "foo", + reference: .branch("main")) + .save(on: app.db) try await Version(package: pkg, - latest: .release, - packageName: "foo", - reference: .tag(2, 0, 0)) - .save(on: app.db) + latest: .release, + packageName: "foo", + reference: .tag(2, 0, 0)) + .save(on: app.db) try await Version(package: pkg, - latest: .preRelease, // this should have been nil - ensure it's reset - packageName: "foo", - reference: .tag(2, 0, 0, "rc1")) - .save(on: app.db) + latest: .preRelease, // this should have been nil - ensure it's reset + packageName: "foo", + reference: .tag(2, 0, 0, "rc1")) + .save(on: app.db) let jpr = try await Package.fetchCandidate(app.db, id: pkg.id!) // MUT @@ -1225,23 +1254,27 @@ class AnalyzerTests: AppTestCase { } func test_trimCheckouts() throws { - // setup - Current.fileManager.checkoutsDirectory = { "/checkouts" } - Current.fileManager.contentsOfDirectory = { @Sendable _ in ["foo", "bar"] } - Current.fileManager.attributesOfItem = { @Sendable path in - [ - "/checkouts/foo": [FileAttributeKey.modificationDate: Current.date().adding(days: -31)], - "/checkouts/bar": [FileAttributeKey.modificationDate: Current.date().adding(days: -29)], - ][path]! - } - let removedPaths = NIOLockedValueBox<[String]>([]) - Current.fileManager.removeItem = { @Sendable p in removedPaths.withLockedValue { $0.append(p) } } + try withDependencies { + $0.date.now = .t0 + } operation: { + // setup + Current.fileManager.checkoutsDirectory = { "/checkouts" } + Current.fileManager.contentsOfDirectory = { @Sendable _ in ["foo", "bar"] } + Current.fileManager.attributesOfItem = { @Sendable path in + [ + "/checkouts/foo": [FileAttributeKey.modificationDate: Date.t0.adding(days: -31)], + "/checkouts/bar": [FileAttributeKey.modificationDate: Date.t0.adding(days: -29)], + ][path]! + } + let removedPaths = NIOLockedValueBox<[String]>([]) + Current.fileManager.removeItem = { @Sendable p in removedPaths.withLockedValue { $0.append(p) } } - // MUT - try Analyze.trimCheckouts() + // MUT + try Analyze.trimCheckouts() - // validate - XCTAssertEqual(removedPaths.withLockedValue { $0 }, ["/checkouts/foo"]) + // validate + XCTAssertEqual(removedPaths.withLockedValue { $0 }, ["/checkouts/foo"]) + } } func test_issue_2571_tags() async throws { @@ -1349,168 +1382,176 @@ class AnalyzerTests: AppTestCase { func test_issue_2571_latest_version() async throws { // Ensure `latest` remains set in case of AppError.noValidVersions // https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/2571 - let pkgId = UUID() - let pkg = Package(id: pkgId, url: "1".asGithubUrl.url, processingStage: .ingestion) - try await pkg.save(on: app.db) - try await Repository(package: pkg, - defaultBranch: "main", - name: "1", - owner: "foo").save(on: app.db) - try await Version(package: pkg, - commit: "commit0", - commitDate: .t0, - latest: .defaultBranch, - packageName: "foo-1", - reference: .branch("main")).save(on: app.db) - try await Version(package: pkg, - commit: "commit0", - commitDate: .t0, - latest: .release, - packageName: "foo-1", - reference: .tag(1, 0, 0)).save(on: app.db) - Current.fileManager.fileExists = { @Sendable _ in true } - Current.git.commitCount = { @Sendable _ in 2 } - Current.git.firstCommitDate = { @Sendable _ in .t0 } - Current.git.hasBranch = { @Sendable _, _ in true } - Current.git.lastCommitDate = { @Sendable _ in .t1 } - struct Error: Swift.Error { } - Current.git.shortlog = { @Sendable _ in + try await withDependencies { + $0.date.now = .now + } operation: { + let pkgId = UUID() + let pkg = Package(id: pkgId, url: "1".asGithubUrl.url, processingStage: .ingestion) + try await pkg.save(on: app.db) + try await Repository(package: pkg, + defaultBranch: "main", + name: "1", + owner: "foo").save(on: app.db) + try await Version(package: pkg, + commit: "commit0", + commitDate: .t0, + latest: .defaultBranch, + packageName: "foo-1", + reference: .branch("main")).save(on: app.db) + try await Version(package: pkg, + commit: "commit0", + commitDate: .t0, + latest: .release, + packageName: "foo-1", + reference: .tag(1, 0, 0)).save(on: app.db) + Current.fileManager.fileExists = { @Sendable _ in true } + Current.git.commitCount = { @Sendable _ in 2 } + Current.git.firstCommitDate = { @Sendable _ in .t0 } + Current.git.hasBranch = { @Sendable _, _ in true } + Current.git.lastCommitDate = { @Sendable _ in .t1 } + struct Error: Swift.Error { } + Current.git.shortlog = { @Sendable _ in """ 1\tPerson 1 1\tPerson 2 """ - } - Current.git.getTags = {@Sendable _ in [.tag(1, 0, 0)] } - Current.shell.run = { @Sendable cmd, path in return "" } - - do { // ensure happy path passes test (no revision changes) - Current.git.revisionInfo = { @Sendable ref, _ in - switch ref { - case .tag(.init(1, 0, 0), "1.0.0"): - return .init(commit: "commit0", date: .t0) - case .branch("main"): - return .init(commit: "commit0", date: .t0) - default: - throw Error() - } } + Current.git.getTags = {@Sendable _ in [.tag(1, 0, 0)] } + Current.shell.run = { @Sendable cmd, path in return "" } + + do { // ensure happy path passes test (no revision changes) + Current.git.revisionInfo = { @Sendable ref, _ in + switch ref { + case .tag(.init(1, 0, 0), "1.0.0"): + return .init(commit: "commit0", date: .t0) + case .branch("main"): + return .init(commit: "commit0", date: .t0) + default: + throw Error() + } + } - // MUT - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(1)) - - // validate versions - let p = try await Package.find(pkgId, on: app.db).unwrap() - try await p.$versions.load(on: app.db) - let versions = p.versions.sorted(by: { $0.reference.description < $1.reference.description }) - XCTAssertEqual(versions.map(\.reference.description), ["1.0.0", "main"]) - XCTAssertEqual(versions.map(\.latest), [.release, .defaultBranch]) - } + // MUT + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(1)) + + // validate versions + let p = try await Package.find(pkgId, on: app.db).unwrap() + try await p.$versions.load(on: app.db) + let versions = p.versions.sorted(by: { $0.reference.description < $1.reference.description }) + XCTAssertEqual(versions.map(\.reference.description), ["1.0.0", "main"]) + XCTAssertEqual(versions.map(\.latest), [.release, .defaultBranch]) + } - // make package available for analysis again - pkg.processingStage = .ingestion - try await pkg.save(on: app.db) + // make package available for analysis again + pkg.processingStage = .ingestion + try await pkg.save(on: app.db) - do { // simulate "main" branch moving forward to ("commit0", .t1) - Current.git.revisionInfo = { @Sendable ref, _ in - switch ref { - case .tag(.init(1, 0, 0), "1.0.0"): - return .init(commit: "commit0", date: .t0) - case .branch("main"): - // main branch has new commit - return .init(commit: "commit1", date: .t1) - default: - throw Error() + do { // simulate "main" branch moving forward to ("commit0", .t1) + Current.git.revisionInfo = { @Sendable ref, _ in + switch ref { + case .tag(.init(1, 0, 0), "1.0.0"): + return .init(commit: "commit0", date: .t0) + case .branch("main"): + // main branch has new commit + return .init(commit: "commit1", date: .t1) + default: + throw Error() + } } - } - Current.shell.run = { @Sendable cmd, path in - // simulate error in getPackageInfo by failing checkout - if cmd == .gitCheckout(branch: "main") { - throw Error() + Current.shell.run = { @Sendable cmd, path in + // simulate error in getPackageInfo by failing checkout + if cmd == .gitCheckout(branch: "main") { + throw Error() + } + return "" } - return "" - } - // MUT - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(1)) + // MUT + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(1)) - // validate error logs - try logger.logs.withValue { logs in - XCTAssertEqual(logs.count, 2) - let error = try logs.last.unwrap() - XCTAssertTrue(error.message.contains("AppError.noValidVersions"), "was: \(error.message)") + // validate error logs + try logger.logs.withValue { logs in + XCTAssertEqual(logs.count, 2) + let error = try logs.last.unwrap() + XCTAssertTrue(error.message.contains("AppError.noValidVersions"), "was: \(error.message)") + } + // validate versions + let p = try await Package.find(pkgId, on: app.db).unwrap() + try await p.$versions.load(on: app.db) + let versions = p.versions.sorted(by: { $0.reference.description < $1.reference.description }) + XCTAssertEqual(versions.map(\.reference.description), ["1.0.0", "main"]) + XCTAssertEqual(versions.map(\.latest), [.release, .defaultBranch]) } - // validate versions - let p = try await Package.find(pkgId, on: app.db).unwrap() - try await p.$versions.load(on: app.db) - let versions = p.versions.sorted(by: { $0.reference.description < $1.reference.description }) - XCTAssertEqual(versions.map(\.reference.description), ["1.0.0", "main"]) - XCTAssertEqual(versions.map(\.latest), [.release, .defaultBranch]) } } func test_issue_2873() async throws { // Ensure we preserve dependency counts from previous default branch version // https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/2873 - // setup - let pkg = try await savePackage(on: app.db, id: .id0, "https://github.com/foo/1".url, processingStage: .ingestion) - try await Repository(package: pkg, - defaultBranch: "main", - name: "1", - owner: "foo", - stars: 100).save(on: app.db) - Current.git.commitCount = { @Sendable _ in 12 } - Current.git.getTags = { @Sendable _ in [] } - Current.git.hasBranch = { @Sendable _, _ in true } - Current.git.firstCommitDate = { @Sendable _ in .t0 } - Current.git.lastCommitDate = { @Sendable _ in .t1 } - Current.git.revisionInfo = { @Sendable _, _ in .init(commit: "sha1", date: .t0) } - Current.git.shortlog = { @Sendable _ in "10\tPerson 1" } - Current.shell.run = { @Sendable cmd, path in - if cmd == .swiftDumpPackage { return .packageDump(name: "foo1") } - return "" - } - - // MUT and validation - - // first analysis pass - try await Analyze.analyze(client: app.client, database: app.db, mode: .id(.id0)) - do { // validate - let pkg = try await Package.query(on: app.db).first() - // numberOfDependencies is nil here, because we've not yet received the info back from the build - XCTAssertEqual(pkg?.scoreDetails?.numberOfDependencies, nil) - } - - do { // receive build report - we could send an actual report here via the API but let's just update - // the field directly instead, we're not testing build reporting after all - let version = try await Version.query(on: app.db).first() - version?.resolvedDependencies = .some([.init(packageName: "dep", - repositoryURL: "https://github.com/some/dep")]) - try await version?.save(on: app.db) - } - - // second analysis pass - try await Analyze.analyze(client: app.client, database: app.db, mode: .id(.id0)) - do { // validate - let pkg = try await Package.query(on: app.db).first() - // numberOfDependencies is 1 now, because we see the updated version - XCTAssertEqual(pkg?.scoreDetails?.numberOfDependencies, 1) - } - - // now we simulate a new version on the default branch - Current.git.revisionInfo = { @Sendable _, _ in .init(commit: "sha2", date: .t1) } - - // third analysis pass - try await Analyze.analyze(client: app.client, database: app.db, mode: .id(.id0)) - do { // validate - let pkg = try await Package.query(on: app.db).first() - // numberOfDependencies must be preserved as 1, even though we've not built this version yet - XCTAssertEqual(pkg?.scoreDetails?.numberOfDependencies, 1) + try await withDependencies { + $0.date.now = .now + } operation: { + // setup + let pkg = try await savePackage(on: app.db, id: .id0, "https://github.com/foo/1".url, processingStage: .ingestion) + try await Repository(package: pkg, + defaultBranch: "main", + name: "1", + owner: "foo", + stars: 100).save(on: app.db) + Current.git.commitCount = { @Sendable _ in 12 } + Current.git.getTags = { @Sendable _ in [] } + Current.git.hasBranch = { @Sendable _, _ in true } + Current.git.firstCommitDate = { @Sendable _ in .t0 } + Current.git.lastCommitDate = { @Sendable _ in .t1 } + Current.git.revisionInfo = { @Sendable _, _ in .init(commit: "sha1", date: .t0) } + Current.git.shortlog = { @Sendable _ in "10\tPerson 1" } + Current.shell.run = { @Sendable cmd, path in + if cmd == .swiftDumpPackage { return .packageDump(name: "foo1") } + return "" + } + + // MUT and validation + + // first analysis pass + try await Analyze.analyze(client: app.client, database: app.db, mode: .id(.id0)) + do { // validate + let pkg = try await Package.query(on: app.db).first() + // numberOfDependencies is nil here, because we've not yet received the info back from the build + XCTAssertEqual(pkg?.scoreDetails?.numberOfDependencies, nil) + } + + do { // receive build report - we could send an actual report here via the API but let's just update + // the field directly instead, we're not testing build reporting after all + let version = try await Version.query(on: app.db).first() + version?.resolvedDependencies = .some([.init(packageName: "dep", + repositoryURL: "https://github.com/some/dep")]) + try await version?.save(on: app.db) + } + + // second analysis pass + try await Analyze.analyze(client: app.client, database: app.db, mode: .id(.id0)) + do { // validate + let pkg = try await Package.query(on: app.db).first() + // numberOfDependencies is 1 now, because we see the updated version + XCTAssertEqual(pkg?.scoreDetails?.numberOfDependencies, 1) + } + + // now we simulate a new version on the default branch + Current.git.revisionInfo = { @Sendable _, _ in .init(commit: "sha2", date: .t1) } + + // third analysis pass + try await Analyze.analyze(client: app.client, database: app.db, mode: .id(.id0)) + do { // validate + let pkg = try await Package.query(on: app.db).first() + // numberOfDependencies must be preserved as 1, even though we've not built this version yet + XCTAssertEqual(pkg?.scoreDetails?.numberOfDependencies, 1) + } } - } +} } diff --git a/Tests/AppTests/AnalyzerVersionThrottlingTests.swift b/Tests/AppTests/AnalyzerVersionThrottlingTests.swift index 256422c94..36654a083 100644 --- a/Tests/AppTests/AnalyzerVersionThrottlingTests.swift +++ b/Tests/AppTests/AnalyzerVersionThrottlingTests.swift @@ -12,199 +12,230 @@ // See the License for the specific language governing permissions and // limitations under the License. +import XCTest + @testable import App -import XCTest +import Dependencies class AnalyzerVersionThrottlingTests: AppTestCase { func test_throttle_keep_old() async throws { // Test keeping old when within throttling window - // setup - Current.date = { .t0 } - let pkg = Package(url: "1") - try await pkg.save(on: app.db) - let old = try makeVersion(pkg, "sha_old", -.hours(23), .branch("main")) - let new = try makeVersion(pkg, "sha_new", -.hours(1), .branch("main")) + try await withDependencies { + $0.date.now = .t0 + } operation: { + // setup + let pkg = Package(url: "1") + try await pkg.save(on: app.db) + let old = try makeVersion(pkg, "sha_old", -.hours(23), .branch("main")) + let new = try makeVersion(pkg, "sha_new", -.hours(1), .branch("main")) - // MUT - let res = Analyze.throttle(latestExistingVersion: old, incoming: [new]) + // MUT + let res = Analyze.throttle(latestExistingVersion: old, incoming: [new]) - // validate - XCTAssertEqual(res, [old]) + // validate + XCTAssertEqual(res, [old]) + } } func test_throttle_take_new() async throws { // Test picking new version when old one is outside the window - // setup - Current.date = { .t0 } - let pkg = Package(url: "1") - try await pkg.save(on: app.db) - let old = try makeVersion(pkg, "sha_old", .hours(-26), .branch("main")) - let new = try makeVersion(pkg, "sha_new", .hours(-1), .branch("main")) + try await withDependencies { + $0.date.now = .t0 + } operation: { + // setup + let pkg = Package(url: "1") + try await pkg.save(on: app.db) + let old = try makeVersion(pkg, "sha_old", .hours(-26), .branch("main")) + let new = try makeVersion(pkg, "sha_new", .hours(-1), .branch("main")) - // MUT - let res = Analyze.throttle(latestExistingVersion: old, incoming: [new]) + // MUT + let res = Analyze.throttle(latestExistingVersion: old, incoming: [new]) - // validate - XCTAssertEqual(res, [new]) + // validate + XCTAssertEqual(res, [new]) + } } func test_throttle_ignore_tags() async throws { // Test to ensure tags are exempt from throttling - // setup - Current.date = { .t0 } - let pkg = Package(url: "1") - try await pkg.save(on: app.db) - let old = try makeVersion(pkg, "sha_old", .hours(-23), .tag(1, 0, 0)) - let new = try makeVersion(pkg, "sha_new", .hours(-1), .tag(2, 0, 0)) + try await withDependencies { + $0.date.now = .t0 + } operation: { + // setup + let pkg = Package(url: "1") + try await pkg.save(on: app.db) + let old = try makeVersion(pkg, "sha_old", .hours(-23), .tag(1, 0, 0)) + let new = try makeVersion(pkg, "sha_new", .hours(-1), .tag(2, 0, 0)) - // MUT - let res = Analyze.throttle(latestExistingVersion: old, incoming: [new]) + // MUT + let res = Analyze.throttle(latestExistingVersion: old, incoming: [new]) - // validate - XCTAssertEqual(res, [new]) + // validate + XCTAssertEqual(res, [new]) + } } func test_throttle_new_package() async throws { // Test picking up a new package's branch - // setup - Current.date = { .t0 } - let pkg = Package(url: "1") - try await pkg.save(on: app.db) - let new = try makeVersion(pkg, "sha_new", .hours(-1), .branch("main")) + try await withDependencies { + $0.date.now = .t0 + } operation: { + // setup + let pkg = Package(url: "1") + try await pkg.save(on: app.db) + let new = try makeVersion(pkg, "sha_new", .hours(-1), .branch("main")) - // MUT - let res = Analyze.throttle(latestExistingVersion: nil, incoming: [new]) + // MUT + let res = Analyze.throttle(latestExistingVersion: nil, incoming: [new]) - // validate - XCTAssertEqual(res, [new]) + // validate + XCTAssertEqual(res, [new]) + } } func test_throttle_branch_ref_change() async throws { // Test behaviour when changing default branch names // Changed to return [new] to avoid branch renames causing 404s // https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/2217 - // setup - Current.date = { .t0 } - let pkg = Package(url: "1") - try await pkg.save(on: app.db) - let old = try makeVersion(pkg, "sha_old", .hours(-23), .branch("develop")) - let new = try makeVersion(pkg, "sha_new", .hours(-1), .branch("main")) + try await withDependencies { + $0.date.now = .t0 + } operation: { + // setup + let pkg = Package(url: "1") + try await pkg.save(on: app.db) + let old = try makeVersion(pkg, "sha_old", .hours(-23), .branch("develop")) + let new = try makeVersion(pkg, "sha_new", .hours(-1), .branch("main")) - // MUT - let res = Analyze.throttle(latestExistingVersion: old, incoming: [new]) + // MUT + let res = Analyze.throttle(latestExistingVersion: old, incoming: [new]) - // validate - XCTAssertEqual(res, [new]) + // validate + XCTAssertEqual(res, [new]) + } } func test_throttle_rename() async throws { // Ensure incoming branch renames are throttled // Changed to return [new] to avoid branch renames causing 404s // https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/2217 - // setup - Current.date = { .t0 } - let pkg = Package(url: "1") - try await pkg.save(on: app.db) - let old = try makeVersion(pkg, "sha", .hours(-1), .branch("main-old")) - let new = try makeVersion(pkg, "sha", .hours(-1), .branch("main-new")) + try await withDependencies { + $0.date.now = .t0 + } operation: { + // setup + let pkg = Package(url: "1") + try await pkg.save(on: app.db) + let old = try makeVersion(pkg, "sha", .hours(-1), .branch("main-old")) + let new = try makeVersion(pkg, "sha", .hours(-1), .branch("main-new")) - // MUT - let res = Analyze.throttle(latestExistingVersion: old, incoming: [new]) + // MUT + let res = Analyze.throttle(latestExistingVersion: old, incoming: [new]) - // validate - XCTAssertEqual(res, [new]) + // validate + XCTAssertEqual(res, [new]) + } } func test_throttle_multiple_incoming_branches_keep_old() async throws { // Test behaviour with multiple incoming branch revisions // NB: this is a theoretical scenario, in practise there should only // ever be one branch revision among the incoming revisions. - // setup - Current.date = { .t0 } - let pkg = Package(url: "1") - try await pkg.save(on: app.db) - let old = try makeVersion(pkg, "sha_old", .hours(-23), .branch("main")) - let new0 = try makeVersion(pkg, "sha_new0", .hours(-3), .branch("main")) - let new1 = try makeVersion(pkg, "sha_new1", .hours(-2), .branch("main")) - let new2 = try makeVersion(pkg, "sha_new2", .hours(-1), .branch("main")) + try await withDependencies { + $0.date.now = .t0 + } operation: { + // setup + let pkg = Package(url: "1") + try await pkg.save(on: app.db) + let old = try makeVersion(pkg, "sha_old", .hours(-23), .branch("main")) + let new0 = try makeVersion(pkg, "sha_new0", .hours(-3), .branch("main")) + let new1 = try makeVersion(pkg, "sha_new1", .hours(-2), .branch("main")) + let new2 = try makeVersion(pkg, "sha_new2", .hours(-1), .branch("main")) - // MUT - let res = Analyze.throttle(latestExistingVersion: old, - incoming: [new0, new1, new2].shuffled()) + // MUT + let res = Analyze.throttle(latestExistingVersion: old, + incoming: [new0, new1, new2].shuffled()) - // validate - XCTAssertEqual(res, [old]) + // validate + XCTAssertEqual(res, [old]) + } } func test_throttle_multiple_incoming_branches_take_new() async throws { // Test behaviour with multiple incoming branch revisions // NB: this is a theoretical scenario, in practise there should only // ever be one branch revision among the incoming revisions. - // setup - Current.date = { .t0 } - let pkg = Package(url: "1") - try await pkg.save(on: app.db) - let old = try makeVersion(pkg, "sha_old", .hours(-26), .branch("main")) - let new0 = try makeVersion(pkg, "sha_new0", .hours(-3), .branch("main")) - let new1 = try makeVersion(pkg, "sha_new1", .hours(-2), .branch("main")) - let new2 = try makeVersion(pkg, "sha_new2", .hours(-1), .branch("main")) + try await withDependencies { + $0.date.now = .t0 + } operation: { + // setup + let pkg = Package(url: "1") + try await pkg.save(on: app.db) + let old = try makeVersion(pkg, "sha_old", .hours(-26), .branch("main")) + let new0 = try makeVersion(pkg, "sha_new0", .hours(-3), .branch("main")) + let new1 = try makeVersion(pkg, "sha_new1", .hours(-2), .branch("main")) + let new2 = try makeVersion(pkg, "sha_new2", .hours(-1), .branch("main")) - // MUT - let res = Analyze.throttle(latestExistingVersion: old, - incoming: [new0, new1, new2].shuffled()) + // MUT + let res = Analyze.throttle(latestExistingVersion: old, + incoming: [new0, new1, new2].shuffled()) - // validate - XCTAssertEqual(res, [new2]) + // validate + XCTAssertEqual(res, [new2]) + } } func test_diffVersions() async throws { // Test that diffVersions applies throttling - // setup - Current.date = { .t0 } - Current.git.getTags = { @Sendable _ in [.branch("main")] } - Current.git.hasBranch = { @Sendable _, _ in true } - let pkg = Package(url: "1".asGithubUrl.url) - try await pkg.save(on: app.db) - try await Repository(package: pkg, defaultBranch: "main").save(on: app.db) - let old = try makeVersion(pkg, "sha_old", .hours(-23), .branch("main"), .defaultBranch) - try await old.save(on: app.db) - let jpr = try await Package.fetchCandidate(app.db, id: pkg.id!) - - do { // keep old version if too soon - Current.git.revisionInfo = { @Sendable _, _ in - .init(commit: "sha_new", date: .t0.addingTimeInterval(.hours(-1)) ) + try await withDependencies { + $0.date.now = .t0 + } operation: { + // setup + Current.git.getTags = { @Sendable _ in [.branch("main")] } + Current.git.hasBranch = { @Sendable _, _ in true } + let pkg = Package(url: "1".asGithubUrl.url) + try await pkg.save(on: app.db) + try await Repository(package: pkg, defaultBranch: "main").save(on: app.db) + let old = try makeVersion(pkg, "sha_old", .hours(-23), .branch("main"), .defaultBranch) + try await old.save(on: app.db) + let jpr = try await Package.fetchCandidate(app.db, id: pkg.id!) + + do { // keep old version if too soon + Current.git.revisionInfo = { @Sendable _, _ in + .init(commit: "sha_new", date: .t0.addingTimeInterval(.hours(-1)) ) + } + + // MUT + let res = try await Analyze.diffVersions(client: app.client, + transaction: app.db, + package: jpr) + + // validate + XCTAssertEqual(res.toAdd, []) + XCTAssertEqual(res.toDelete, []) + XCTAssertEqual(res.toKeep, [old]) } - // MUT - let res = try await Analyze.diffVersions(client: app.client, - transaction: app.db, - package: jpr) - - // validate - XCTAssertEqual(res.toAdd, []) - XCTAssertEqual(res.toDelete, []) - XCTAssertEqual(res.toKeep, [old]) - } - - do { // new version must come through - Current.date = { .t0.addingTimeInterval(.hours(2)) } - Current.git.revisionInfo = { @Sendable _, _ in - // now simulate a newer branch revision - .init(commit: "sha_new2", date: .t0.addingTimeInterval(.hours(2)) ) + try await withDependencies { + $0.date.now = .t0.addingTimeInterval(.hours(2)) + } operation: { + // new version must come through + Current.git.revisionInfo = { @Sendable _, _ in + // now simulate a newer branch revision + .init(commit: "sha_new2", date: .t0.addingTimeInterval(.hours(2)) ) + } + + // MUT + let res = try await Analyze.diffVersions(client: app.client, + transaction: app.db, + package: jpr) + + // validate + XCTAssertEqual(res.toAdd.map(\.commit), ["sha_new2"]) + XCTAssertEqual(res.toDelete, [old]) + XCTAssertEqual(res.toKeep, []) } - - // MUT - let res = try await Analyze.diffVersions(client: app.client, - transaction: app.db, - package: jpr) - - // validate - XCTAssertEqual(res.toAdd.map(\.commit), ["sha_new2"]) - XCTAssertEqual(res.toDelete, [old]) - XCTAssertEqual(res.toKeep, []) } } @@ -244,8 +275,10 @@ class AnalyzerVersionThrottlingTests: AppTestCase { .t0.addingTimeInterval(.hours(25)), ] - do { // start with a branch revision - Current.date = { commitDates[0] } + try await withDependencies { + $0.date.now = commitDates[0] + } operation: { + // start with a branch revision Current.git.revisionInfo = { @Sendable _, _ in .init(commit: "sha0", date: commitDates[0] ) } let delta = try await runVersionReconciliation() @@ -254,8 +287,10 @@ class AnalyzerVersionThrottlingTests: AppTestCase { XCTAssertEqual(delta.toKeep, []) } - do { // one hour later a new commit landed - which should be ignored - Current.date = { commitDates[1] } + try await withDependencies { + $0.date.now = commitDates[1] + } operation: { + // one hour later a new commit landed - which should be ignored Current.git.revisionInfo = { @Sendable _, _ in .init(commit: "sha1", date: commitDates[1] ) } let delta = try await runVersionReconciliation() @@ -264,9 +299,11 @@ class AnalyzerVersionThrottlingTests: AppTestCase { XCTAssertEqual(delta.toKeep.map(\.commit), ["sha0"]) } - do { // run another 5 commits every four hours - they all should be ignored - for idx in 2...6 { - Current.date = { commitDates[idx] } + // run another 5 commits every four hours - they all should be ignored + for idx in 2...6 { + try await withDependencies { + $0.date.now = commitDates[idx] + } operation: { Current.git.revisionInfo = { @Sendable _, _ in .init(commit: "sha\(idx)", date: commitDates[idx] ) } let delta = try await runVersionReconciliation() @@ -276,8 +313,10 @@ class AnalyzerVersionThrottlingTests: AppTestCase { } } - do { // advancing another 4 hours for a total of 25 hours should finally create a new version - Current.date = { commitDates[7] } + try await withDependencies { + $0.date.now = commitDates[7] + } operation: { + // advancing another 4 hours for a total of 25 hours should finally create a new version Current.git.revisionInfo = { @Sendable _, _ in .init(commit: "sha7", date: commitDates[7] ) } let delta = try await runVersionReconciliation() @@ -294,41 +333,44 @@ class AnalyzerVersionThrottlingTests: AppTestCase { // the "existing" (ex) revision, replacing it with an older "incoming" // (inc) revision. // setup - Current.date = { .t0 } - let pkg = Package(url: "1") - try await pkg.save(on: app.db) - - do { // both within window - let ex = try makeVersion(pkg, "sha-ex", .hours(-1), .branch("main")) - let inc = try makeVersion(pkg, "sha-inc", .hours(-23), .branch("main")) - - // MUT - let res = Analyze.throttle(latestExistingVersion: ex, incoming: [inc]) - - // validate - XCTAssertEqual(res, [ex]) - } - - do { // incoming version out of window - let ex = try makeVersion(pkg, "sha-ex", .hours(-1), .branch("main")) - let inc = try makeVersion(pkg, "sha-inc", .hours(-26), .branch("main")) - - // MUT - let res = Analyze.throttle(latestExistingVersion: ex, incoming: [inc]) - - // validate - XCTAssertEqual(res, [ex]) - } - - do { // both versions out of window - let ex = try makeVersion(pkg, "sha-ex", .hours(-26), .branch("main")) - let inc = try makeVersion(pkg, "sha-inc", .hours(-28), .branch("main")) - - // MUT - let res = Analyze.throttle(latestExistingVersion: ex, incoming: [inc]) - - // validate - XCTAssertEqual(res, [inc]) + try await withDependencies { + $0.date.now = .t0 + } operation: { + let pkg = Package(url: "1") + try await pkg.save(on: app.db) + + do { // both within window + let ex = try makeVersion(pkg, "sha-ex", .hours(-1), .branch("main")) + let inc = try makeVersion(pkg, "sha-inc", .hours(-23), .branch("main")) + + // MUT + let res = Analyze.throttle(latestExistingVersion: ex, incoming: [inc]) + + // validate + XCTAssertEqual(res, [ex]) + } + + do { // incoming version out of window + let ex = try makeVersion(pkg, "sha-ex", .hours(-1), .branch("main")) + let inc = try makeVersion(pkg, "sha-inc", .hours(-26), .branch("main")) + + // MUT + let res = Analyze.throttle(latestExistingVersion: ex, incoming: [inc]) + + // validate + XCTAssertEqual(res, [ex]) + } + + do { // both versions out of window + let ex = try makeVersion(pkg, "sha-ex", .hours(-26), .branch("main")) + let inc = try makeVersion(pkg, "sha-inc", .hours(-28), .branch("main")) + + // MUT + let res = Analyze.throttle(latestExistingVersion: ex, incoming: [inc]) + + // validate + XCTAssertEqual(res, [inc]) + } } } diff --git a/Tests/AppTests/ApiTests.swift b/Tests/AppTests/ApiTests.swift index ad9eb4261..4a66efc7c 100644 --- a/Tests/AppTests/ApiTests.swift +++ b/Tests/AppTests/ApiTests.swift @@ -14,6 +14,7 @@ @testable import App +import Dependencies import PackageCollectionsSigning import SnapshotTesting import XCTVapor @@ -66,7 +67,7 @@ class ApiTests: AppTestCase { try await Version(package: p2, packageName: "Bar", reference: .branch("main")).save(on: app.db) try await Search.refresh(on: app.db) - let event = ActorIsolated(nil) + let event = App.ActorIsolated(nil) Current.postPlausibleEvent = { @Sendable _, kind, path, _ in await event.setValue(.init(kind: kind, path: path)) } @@ -738,7 +739,7 @@ class ApiTests: AppTestCase { try await Build(version: v, platform: .macosXcodebuild, status: .ok, swiftVersion: .v1) .save(on: app.db) - let event = ActorIsolated(nil) + let event = App.ActorIsolated(nil) Current.postPlausibleEvent = { @Sendable _, kind, path, _ in await event.setValue(.init(kind: kind, path: path)) } @@ -788,33 +789,35 @@ class ApiTests: AppTestCase { func test_package_collections_owner() async throws { try XCTSkipIf(!isRunningInCI && Current.collectionSigningPrivateKey() == nil, "Skip test for local user due to unset COLLECTION_SIGNING_PRIVATE_KEY env variable") - // setup - Current.date = { .t0 } - Current.apiSigningKey = { "secret" } - let p1 = Package(id: .id1, url: "1") - try await p1.save(on: app.db) - try await Repository(package: p1, - defaultBranch: "main", - name: "name 1", - owner: "foo", - summary: "foo bar package").save(on: app.db) - let v = try Version(package: p1, - latest: .release, - packageName: "Foo", - reference: .tag(1, 2, 3), - toolsVersion: "5.0") - try await v.save(on: app.db) - try await Product(version: v, type: .library(.automatic), name: "lib") - .save(on: app.db) - try await Search.refresh(on: app.db) + try await withDependencies { + $0.date.now = .t0 + } operation: { + // setup + Current.apiSigningKey = { "secret" } + let p1 = Package(id: .id1, url: "1") + try await p1.save(on: app.db) + try await Repository(package: p1, + defaultBranch: "main", + name: "name 1", + owner: "foo", + summary: "foo bar package").save(on: app.db) + let v = try Version(package: p1, + latest: .release, + packageName: "Foo", + reference: .tag(1, 2, 3), + toolsVersion: "5.0") + try await v.save(on: app.db) + try await Product(version: v, type: .library(.automatic), name: "lib") + .save(on: app.db) + try await Search.refresh(on: app.db) - let event = ActorIsolated(nil) - Current.postPlausibleEvent = { @Sendable _, kind, path, _ in - await event.setValue(.init(kind: kind, path: path)) - } + let event = App.ActorIsolated(nil) + Current.postPlausibleEvent = { @Sendable _, kind, path, _ in + await event.setValue(.init(kind: kind, path: path)) + } - do { // MUT - let body: ByteBuffer = .init(string: """ + do { // MUT + let body: ByteBuffer = .init(string: """ { "revision": 3, "authorName": "author", @@ -832,69 +835,72 @@ class ApiTests: AppTestCase { } """) - try await app.test(.POST, "api/package-collections", - headers: .bearerApplicationJSON(try .apiToken(secretKey: "secret", tier: .tier3)), - body: body, - afterResponse: { res async throws in - // validation - XCTAssertEqual(res.status, .ok) - let container = try res.content.decode(SignedCollection.self) - XCTAssertFalse(container.signature.signature.isEmpty) - // more details are tested in PackageCollectionTests - XCTAssertEqual(container.collection.name, "my collection") - }) - } + try await app.test(.POST, "api/package-collections", + headers: .bearerApplicationJSON(try .apiToken(secretKey: "secret", tier: .tier3)), + body: body, + afterResponse: { res async throws in + // validation + XCTAssertEqual(res.status, .ok) + let container = try res.content.decode(SignedCollection.self) + XCTAssertFalse(container.signature.signature.isEmpty) + // more details are tested in PackageCollectionTests + XCTAssertEqual(container.collection.name, "my collection") + }) + } - // ensure API event has been reported - await event.withValue { - XCTAssertEqual($0, .some(.init(kind: .pageview, path: .packageCollections))) + // ensure API event has been reported + await event.withValue { + XCTAssertEqual($0, .some(.init(kind: .pageview, path: .packageCollections))) + } } } func test_package_collections_packageURLs() async throws { try XCTSkipIf(!isRunningInCI && Current.collectionSigningPrivateKey() == nil, "Skip test for local user due to unset COLLECTION_SIGNING_PRIVATE_KEY env variable") - // setup let refDate = Date(timeIntervalSince1970: 0) - Current.date = { refDate } - Current.apiSigningKey = { "secret" } - let p1 = Package(id: UUID(uuidString: "442cf59f-0135-4d08-be00-bc9a7cebabd3")!, - url: "1") - try await p1.save(on: app.db) - let p2 = Package(id: UUID(uuidString: "4e256250-d1ea-4cdd-9fe9-0fc5dce17a80")!, - url: "2") - try await p2.save(on: app.db) - try await Repository(package: p1, - defaultBranch: "main", - summary: "some package").save(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - name: "name 2", - owner: "foo", - summary: "foo bar package").save(on: app.db) - do { - let v = try Version(package: p1, - latest: .release, - packageName: "Foo", - reference: .tag(1, 2, 3), - toolsVersion: "5.3") - try await v.save(on: app.db) - try await Product(version: v, type: .library(.automatic), name: "p1") - .save(on: app.db) - } - do { - let v = try Version(package: p2, - latest: .release, - packageName: "Bar", - reference: .tag(2, 0, 0), - toolsVersion: "5.4") - try await v.save(on: app.db) - try await Product(version: v, type: .library(.automatic), name: "p2") - .save(on: app.db) - } - try await Search.refresh(on: app.db) - - do { // MUT - let body: ByteBuffer = .init(string: """ + try await withDependencies { + $0.date.now = refDate + } operation: { + // setup + Current.apiSigningKey = { "secret" } + let p1 = Package(id: UUID(uuidString: "442cf59f-0135-4d08-be00-bc9a7cebabd3")!, + url: "1") + try await p1.save(on: app.db) + let p2 = Package(id: UUID(uuidString: "4e256250-d1ea-4cdd-9fe9-0fc5dce17a80")!, + url: "2") + try await p2.save(on: app.db) + try await Repository(package: p1, + defaultBranch: "main", + summary: "some package").save(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + name: "name 2", + owner: "foo", + summary: "foo bar package").save(on: app.db) + do { + let v = try Version(package: p1, + latest: .release, + packageName: "Foo", + reference: .tag(1, 2, 3), + toolsVersion: "5.3") + try await v.save(on: app.db) + try await Product(version: v, type: .library(.automatic), name: "p1") + .save(on: app.db) + } + do { + let v = try Version(package: p2, + latest: .release, + packageName: "Bar", + reference: .tag(2, 0, 0), + toolsVersion: "5.4") + try await v.save(on: app.db) + try await Product(version: v, type: .library(.automatic), name: "p2") + .save(on: app.db) + } + try await Search.refresh(on: app.db) + + do { // MUT + let body: ByteBuffer = .init(string: """ { "revision": 3, "authorName": "author", @@ -913,17 +919,18 @@ class ApiTests: AppTestCase { "overview": "my overview" } """) - - try await app.test(.POST, - "api/package-collections", - headers: .bearerApplicationJSON((try .apiToken(secretKey: "secret", tier: .tier3))), - body: body, - afterResponse: { res async throws in - // validation - XCTAssertEqual(res.status, .ok) - let pkgColl = try res.content.decode(PackageCollection.self) - assertSnapshot(of: pkgColl, as: .dump) - }) + + try await app.test(.POST, + "api/package-collections", + headers: .bearerApplicationJSON((try .apiToken(secretKey: "secret", tier: .tier3))), + body: body, + afterResponse: { res async throws in + // validation + XCTAssertEqual(res.status, .ok) + let pkgColl = try res.content.decode(PackageCollection.self) + assertSnapshot(of: pkgColl, as: .dump) + }) + } } } diff --git a/Tests/AppTests/BuildMonitorControllerTests.swift b/Tests/AppTests/BuildMonitorControllerTests.swift index e259daed2..c80b99caa 100644 --- a/Tests/AppTests/BuildMonitorControllerTests.swift +++ b/Tests/AppTests/BuildMonitorControllerTests.swift @@ -12,15 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. +import XCTest + @testable import App +import Dependencies import Vapor -import XCTest + class BuildMonitorControllerTests: AppTestCase { func test_show_owner() async throws { - do { + try await withDependencies { + $0.date.now = .now + } operation: { let package = try await savePackage(on: app.db, "https://github.com/daveverwer/LeftPad") let version = try Version(package: package) try await version.save(on: app.db) @@ -29,11 +34,11 @@ class BuildMonitorControllerTests: AppTestCase { status: .ok, swiftVersion: .init(5, 6, 0)).save(on: app.db) try await Repository(package: package).save(on: app.db) + + // MUT + try await app.test(.GET, "/build-monitor", afterResponse: { response async in + XCTAssertEqual(response.status, .ok) + }) } - - // MUT - try await app.test(.GET, "/build-monitor", afterResponse: { response async in - XCTAssertEqual(response.status, .ok) - }) } } diff --git a/Tests/AppTests/ErrorReportingTests.swift b/Tests/AppTests/ErrorReportingTests.swift index 4233128e8..64e776b93 100644 --- a/Tests/AppTests/ErrorReportingTests.swift +++ b/Tests/AppTests/ErrorReportingTests.swift @@ -14,6 +14,7 @@ @testable import App +import Dependencies import XCTVapor @@ -36,8 +37,12 @@ class ErrorReportingTests: AppTestCase { try await Package(url: "1", processingStage: .reconciliation).save(on: app.db) Current.fetchMetadata = { _, _, _ in throw Github.Error.invalidURI(nil, "1") } - // MUT - try await ingest(client: app.client, database: app.db, mode: .limit(10)) + try await withDependencies { + $0.date.now = .now + } operation: { + // MUT + try await ingest(client: app.client, database: app.db, mode: .limit(10)) + } // validation logger.logs.withValue { diff --git a/Tests/AppTests/HomeIndexModelTests.swift b/Tests/AppTests/HomeIndexModelTests.swift index 5c6e01457..ec634c995 100644 --- a/Tests/AppTests/HomeIndexModelTests.swift +++ b/Tests/AppTests/HomeIndexModelTests.swift @@ -14,6 +14,7 @@ @testable import App +import Dependencies import XCTVapor @@ -36,45 +37,49 @@ class HomeIndexModelTests: AppTestCase { // Sleep for 1ms to ensure we can detect a difference between update times. try await Task.sleep(nanoseconds: UInt64(1e6)) - // MUT - let m = try await HomeIndex.Model.query(database: app.db) + try await withDependencies { + $0.date.now = .now + } operation: { + // MUT + let m = try await HomeIndex.Model.query(database: app.db) - // validate - let createdAt = try XCTUnwrap(pkg.createdAt) + // validate + let createdAt = try XCTUnwrap(pkg.createdAt) #if os(Linux) - if m.recentPackages == [ - .init( - date: createdAt, - link: .init(label: "Package", url: "/foo/1") - ) - ] { - logWarning() - // When this triggers, remove Task.sleep above and the validtion below until // TEMPORARY - END - // and replace with original assert: - // XCTAssertEqual(m.recentPackages, [ - // .init( - // date: createdAt, - // link: .init(label: "Package", url: "/foo/1") - // ) - // ]) - } + if m.recentPackages == [ + .init( + date: createdAt, + link: .init(label: "Package", url: "/foo/1") + ) + ] { + logWarning() + // When this triggers, remove Task.sleep above and the validtion below until // TEMPORARY - END + // and replace with original assert: + // XCTAssertEqual(m.recentPackages, [ + // .init( + // date: createdAt, + // link: .init(label: "Package", url: "/foo/1") + // ) + // ]) + } #endif - XCTAssertEqual(m.recentPackages.count, 1) - let recent = try XCTUnwrap(m.recentPackages.first) - // Comaring the dates directly fails due to tiny rounding differences with the new swift-foundation types on Linux - // E.g. - // 1724071056.5824609 - // 1724071056.5824614 - // By testing only to accuracy 10e-5 and delaying by 10e-3 we ensure we properly detect if the value was changed. - XCTAssertEqual(recent.date.timeIntervalSince1970, createdAt.timeIntervalSince1970, accuracy: 10e-5) - XCTAssertEqual(recent.link, .init(label: "Package", url: "/foo/1")) - XCTAssertEqual(m.recentReleases, [ - .init(packageName: "Package", - version: "1.2.3", - date: "\(date: Date(timeIntervalSince1970: 0), relativeTo: Current.date())", - url: "/foo/1"), - ]) - // TEMPORARY - END + XCTAssertEqual(m.recentPackages.count, 1) + let recent = try XCTUnwrap(m.recentPackages.first) + // Comaring the dates directly fails due to tiny rounding differences with the new swift-foundation types on Linux + // E.g. + // 1724071056.5824609 + // 1724071056.5824614 + // By testing only to accuracy 10e-5 and delaying by 10e-3 we ensure we properly detect if the value was changed. + XCTAssertEqual(recent.date.timeIntervalSince1970, createdAt.timeIntervalSince1970, accuracy: 10e-5) + XCTAssertEqual(recent.link, .init(label: "Package", url: "/foo/1")) + XCTAssertEqual(m.recentReleases, [ + .init(packageName: "Package", + version: "1.2.3", + date: "\(date: Date(timeIntervalSince1970: 0), relativeTo: Date.now)", + url: "/foo/1"), + ]) + // TEMPORARY - END + } } } diff --git a/Tests/AppTests/IngestorTests.swift b/Tests/AppTests/IngestorTests.swift index 0804fe49f..16acd3044 100644 --- a/Tests/AppTests/IngestorTests.swift +++ b/Tests/AppTests/IngestorTests.swift @@ -16,6 +16,7 @@ import XCTest @testable import App +import Dependencies import Fluent import S3Store import Vapor @@ -33,8 +34,12 @@ class IngestorTests: AppTestCase { try await packages.save(on: app.db) let lastUpdate = Date() - // MUT - try await ingest(client: app.client, database: app.db, mode: .limit(10)) + try await withDependencies { + $0.date.now = .now + } operation: { + // MUT + try await ingest(client: app.client, database: app.db, mode: .limit(10)) + } // validate let repos = try await Repository.query(on: app.db).all() @@ -125,7 +130,7 @@ class IngestorTests: AppTestCase { Date(timeIntervalSince1970: 1), ], license: .mit, - openIssues: 1, + openIssues: 1, parentUrl: nil, openPullRequests: 2, owner: "foo", @@ -211,7 +216,7 @@ class IngestorTests: AppTestCase { issuesClosedAtDates: [], license: .mit, openIssues: 1, - parentUrl: nil, + parentUrl: nil, openPullRequests: 2, owner: "foo", pullRequestsClosedAtDates: [], @@ -294,8 +299,12 @@ class IngestorTests: AppTestCase { let packages = testUrls.map { Package(url: $0, processingStage: .reconciliation) } try await packages.save(on: app.db) - // MUT - try await ingest(client: app.client, database: app.db, mode: .limit(testUrls.count)) + try await withDependencies { + $0.date.now = .now + } operation: { + // MUT + try await ingest(client: app.client, database: app.db, mode: .limit(testUrls.count)) + } // validate let repos = try await Repository.query(on: app.db).all() @@ -318,8 +327,12 @@ class IngestorTests: AppTestCase { } let lastUpdate = Date() - // MUT - try await ingest(client: app.client, database: app.db, mode: .limit(10)) + try await withDependencies { + $0.date.now = .now + } operation: { + // MUT + try await ingest(client: app.client, database: app.db, mode: .limit(10)) + } // validate let repos = try await Repository.query(on: app.db).all() @@ -357,7 +370,7 @@ class IngestorTests: AppTestCase { issuesClosedAtDates: [], license: .mit, openIssues: 0, - parentUrl: nil, + parentUrl: nil, openPullRequests: 0, owner: "owner", pullRequestsClosedAtDates: [], @@ -367,8 +380,12 @@ class IngestorTests: AppTestCase { } let lastUpdate = Date() - // MUT - try await ingest(client: app.client, database: app.db, mode: .limit(10)) + try await withDependencies { + $0.date.now = .now + } operation: { + // MUT + try await ingest(client: app.client, database: app.db, mode: .limit(10)) + } // validate repositories (single element pointing to the ingested package) let repos = try await Repository.query(on: app.db).all() @@ -406,82 +423,86 @@ class IngestorTests: AppTestCase { } func test_ingest_storeS3Readme() async throws { - // setup - let app = self.app! - let pkg = Package(url: "https://github.com/foo/bar".url, processingStage: .reconciliation) - try await pkg.save(on: app.db) - Current.fetchMetadata = { _, owner, repository in .mock(owner: owner, repository: repository) } - let fetchCalls = QueueIsolated(0) - Current.fetchReadme = { _, _, _ in - fetchCalls.increment() - if fetchCalls.value <= 2 { - return .init(etag: "etag1", - html: "readme html 1", - htmlUrl: "readme url", - imagesToCache: []) - } else { - return .init(etag: "etag2", - html: "readme html 2", - htmlUrl: "readme url", - imagesToCache: []) + try await withDependencies { + $0.date.now = .now + } operation: { + // setup + let app = self.app! + let pkg = Package(url: "https://github.com/foo/bar".url, processingStage: .reconciliation) + try await pkg.save(on: app.db) + Current.fetchMetadata = { _, owner, repository in .mock(owner: owner, repository: repository) } + let fetchCalls = QueueIsolated(0) + Current.fetchReadme = { _, _, _ in + fetchCalls.increment() + if fetchCalls.value <= 2 { + return .init(etag: "etag1", + html: "readme html 1", + htmlUrl: "readme url", + imagesToCache: []) + } else { + return .init(etag: "etag2", + html: "readme html 2", + htmlUrl: "readme url", + imagesToCache: []) + } } - } - let storeCalls = QueueIsolated(0) - Current.storeS3Readme = { owner, repo, html in - storeCalls.increment() - XCTAssertEqual(owner, "foo") - XCTAssertEqual(repo, "bar") - if fetchCalls.value <= 2 { - XCTAssertEqual(html, "readme html 1") - } else { - XCTAssertEqual(html, "readme html 2") + let storeCalls = QueueIsolated(0) + Current.storeS3Readme = { owner, repo, html in + storeCalls.increment() + XCTAssertEqual(owner, "foo") + XCTAssertEqual(repo, "bar") + if fetchCalls.value <= 2 { + XCTAssertEqual(html, "readme html 1") + } else { + XCTAssertEqual(html, "readme html 2") + } + return "objectUrl" } - return "objectUrl" - } - do { // first ingestion, no readme has been saved - // MUT - try await ingest(client: app.client, database: app.db, mode: .limit(1)) - - // validate - try await XCTAssertEqualAsync(await Repository.query(on: app.db).count(), 1) - let repo = try await XCTUnwrapAsync(await Repository.query(on: app.db).first()) - // Ensure fetch and store have been called, etag save to repository - XCTAssertEqual(fetchCalls.value, 1) - XCTAssertEqual(storeCalls.value, 1) - XCTAssertEqual(repo.s3Readme, .cached(s3ObjectUrl: "objectUrl", githubEtag: "etag1")) - } + do { // first ingestion, no readme has been saved + // MUT + try await ingest(client: app.client, database: app.db, mode: .limit(1)) + + // validate + try await XCTAssertEqualAsync(await Repository.query(on: app.db).count(), 1) + let repo = try await XCTUnwrapAsync(await Repository.query(on: app.db).first()) + // Ensure fetch and store have been called, etag save to repository + XCTAssertEqual(fetchCalls.value, 1) + XCTAssertEqual(storeCalls.value, 1) + XCTAssertEqual(repo.s3Readme, .cached(s3ObjectUrl: "objectUrl", githubEtag: "etag1")) + } - do { // second pass, readme has been saved, no new save should be issued - pkg.processingStage = .reconciliation - try await pkg.save(on: app.db) + do { // second pass, readme has been saved, no new save should be issued + pkg.processingStage = .reconciliation + try await pkg.save(on: app.db) - // MUT - try await ingest(client: app.client, database: app.db, mode: .limit(1)) + // MUT + try await ingest(client: app.client, database: app.db, mode: .limit(1)) - // validate - try await XCTAssertEqualAsync(await Repository.query(on: app.db).count(), 1) - let repo = try await XCTUnwrapAsync(await Repository.query(on: app.db).first()) - // Ensure fetch and store have been called, etag save to repository - XCTAssertEqual(fetchCalls.value, 2) - XCTAssertEqual(storeCalls.value, 1) - XCTAssertEqual(repo.s3Readme, .cached(s3ObjectUrl: "objectUrl", githubEtag: "etag1")) - } + // validate + try await XCTAssertEqualAsync(await Repository.query(on: app.db).count(), 1) + let repo = try await XCTUnwrapAsync(await Repository.query(on: app.db).first()) + // Ensure fetch and store have been called, etag save to repository + XCTAssertEqual(fetchCalls.value, 2) + XCTAssertEqual(storeCalls.value, 1) + XCTAssertEqual(repo.s3Readme, .cached(s3ObjectUrl: "objectUrl", githubEtag: "etag1")) + } - do { // third pass, readme has changed upstream, save should be issues - pkg.processingStage = .reconciliation - try await pkg.save(on: app.db) + do { // third pass, readme has changed upstream, save should be issues + pkg.processingStage = .reconciliation + try await pkg.save(on: app.db) - // MUT - try await ingest(client: app.client, database: app.db, mode: .limit(1)) + // MUT + try await ingest(client: app.client, database: app.db, mode: .limit(1)) - // validate - try await XCTAssertEqualAsync(await Repository.query(on: app.db).count(), 1) - let repo = try await XCTUnwrapAsync(await Repository.query(on: app.db).first()) - // Ensure fetch and store have been called, etag save to repository - XCTAssertEqual(fetchCalls.value, 3) - XCTAssertEqual(storeCalls.value, 2) - XCTAssertEqual(repo.s3Readme, .cached(s3ObjectUrl: "objectUrl", githubEtag: "etag2")) + // validate + try await XCTAssertEqualAsync(await Repository.query(on: app.db).count(), 1) + let repo = try await XCTUnwrapAsync(await Repository.query(on: app.db).first()) + // Ensure fetch and store have been called, etag save to repository + XCTAssertEqual(fetchCalls.value, 3) + XCTAssertEqual(storeCalls.value, 2) + XCTAssertEqual(repo.s3Readme, .cached(s3ObjectUrl: "objectUrl", githubEtag: "etag2")) + } } } @@ -519,8 +540,12 @@ class IngestorTests: AppTestCase { XCTAssertEqual(imagesToCache.count, 2) } - // MUT - try await ingest(client: app.client, database: app.db, mode: .limit(1)) + try await withDependencies { + $0.date.now = .now + } operation: { + // MUT + try await ingest(client: app.client, database: app.db, mode: .limit(1)) + } // There should only be one call as `storeS3ReadmeImages` takes the array of images. XCTAssertEqual(storeS3ReadmeImagesCalls.value, 1) @@ -546,8 +571,13 @@ class IngestorTests: AppTestCase { } do { // first ingestion, no readme has been saved - // MUT - try await ingest(client: app.client, database: app.db, mode: .limit(1)) + try await withDependencies { + $0.date.now = .now + } operation: { + // MUT + let app = self.app! + try await ingest(client: app.client, database: app.db, mode: .limit(1)) + } // validate let app = self.app! @@ -599,7 +629,7 @@ class IngestorTests: AppTestCase { let postMigrationFetchedRepo = try await XCTUnwrapAsync(try await Repository.query(on: app.db).first()) XCTAssertEqual(postMigrationFetchedRepo.s3Readme, .cached(s3ObjectUrl: "object-url", githubEtag: "")) } - + func test_getFork() async throws { try await Package(id: .id0, url: "https://github.com/foo/parent.git".url, processingStage: .analysis).save(on: app.db) try await Package(url: "https://github.com/bar/forked.git", processingStage: .analysis).save(on: app.db) @@ -619,7 +649,7 @@ class IngestorTests: AppTestCase { // test lookup when package is not in the index let fork4 = await getFork(on: app.db, parent: .init(url: "https://github.com/some/other.git")) XCTAssertEqual(fork4, .parentURL("https://github.com/some/other.git")) - + // test lookup when parent url is nil let fork5 = await getFork(on: app.db, parent: nil) XCTAssertEqual(fork5, nil) diff --git a/Tests/AppTests/MastodonTests.swift b/Tests/AppTests/MastodonTests.swift index b15997bd4..2708464dd 100644 --- a/Tests/AppTests/MastodonTests.swift +++ b/Tests/AppTests/MastodonTests.swift @@ -14,18 +14,19 @@ @testable import App -import XCTVapor +import Dependencies import SemanticVersion +import XCTVapor final class MastodonTests: AppTestCase { func test_endToEnd() async throws { // setup - nonisolated(unsafe) var message: String? + let message = QueueIsolated(nil) Current.mastodonPost = { _, msg in - if message == nil { - message = msg + if message.value == nil { + message.setValue(msg) } else { XCTFail("message must only be set once") } @@ -54,46 +55,59 @@ final class MastodonTests: AppTestCase { } return "" } - // run first two processing steps - try await reconcile(client: app.client, database: app.db) - try await ingest(client: app.client, database: app.db, mode: .limit(10)) - - // MUT - analyze, triggering the post - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) - do { - let msg = try XCTUnwrap(message) - XCTAssertTrue(msg.hasPrefix("📦 foo just added a new package, Mock"), "was \(msg)") + + try await withDependencies { + $0.date.now = .now + } operation: { + // run first two processing steps + try await reconcile(client: app.client, database: app.db) + try await ingest(client: app.client, database: app.db, mode: .limit(10)) + + // MUT - analyze, triggering the post + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) + + do { + let msg = try XCTUnwrap(message.value) + XCTAssertTrue(msg.hasPrefix("📦 foo just added a new package, Mock"), "was \(msg)") + } + + // run stages again to simulate the cycle... + message.setValue(nil) + try await reconcile(client: app.client, database: app.db) } - // run stages again to simulate the cycle... - message = nil - try await reconcile(client: app.client, database: app.db) - Current.date = { Date().addingTimeInterval(Constants.reIngestionDeadtime) } - try await ingest(client: app.client, database: app.db, mode: .limit(10)) + try await withDependencies { + $0.date.now = .now.addingTimeInterval(Constants.reIngestionDeadtime) + } operation: { + try await ingest(client: app.client, database: app.db, mode: .limit(10)) - // MUT - analyze, triggering posts if any - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) + // MUT - analyze, triggering posts if any + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) + } // validate - there are no new posts to send - XCTAssertNil(message) + XCTAssertNil(message.value) // Now simulate receiving a package update: version 2.0.0 Current.git.getTags = { @Sendable _ in [.tag(2, 0, 0)] } - // fast forward our clock by the deadtime interval again (*2) and re-ingest - Current.date = { Date().addingTimeInterval(Constants.reIngestionDeadtime * 2) } - try await ingest(client: app.client, database: app.db, mode: .limit(10)) - // MUT - analyze again - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) + try await withDependencies { + // fast forward our clock by the deadtime interval again (*2) and re-ingest + $0.date.now = .now.addingTimeInterval(Constants.reIngestionDeadtime * 2) + } operation: { + try await ingest(client: app.client, database: app.db, mode: .limit(10)) + // MUT - analyze again + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) + } // validate - let msg = try XCTUnwrap(message) + let msg = try XCTUnwrap(message.value) XCTAssertTrue(msg.hasPrefix("⬆️ foo just released Mock v2.0.0"), "was: \(msg)") } diff --git a/Tests/AppTests/Mocks/API.PackageController.GetRoute.Model+mock.swift b/Tests/AppTests/Mocks/API.PackageController.GetRoute.Model+mock.swift index 31da3f38e..c23ef67df 100644 --- a/Tests/AppTests/Mocks/API.PackageController.GetRoute.Model+mock.swift +++ b/Tests/AppTests/Mocks/API.PackageController.GetRoute.Model+mock.swift @@ -12,14 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Foundation + @testable import App -import Foundation +import Dependencies extension API.PackageController.GetRoute.Model { static var mock: Self { - .init( + @Dependency(\.date.now) var now + return .init( packageId: UUID("cafecafe-cafe-cafe-cafe-cafecafecafe")!, repositoryOwner: "Alamo", repositoryOwnerName: "Alamofire", @@ -29,8 +32,8 @@ extension API.PackageController.GetRoute.Model { openIssuesURL: "https://github.com/Alamofire/Alamofire/issues", openPullRequestsCount: 5, openPullRequestsURL: "https://github.com/Alamofire/Alamofire/pulls", - lastIssueClosedAt: Current.date().adding(days: -5), - lastPullRequestClosedAt: Current.date().adding(days: -6) + lastIssueClosedAt: now.adding(days: -5), + lastPullRequestClosedAt: now.adding(days: -6) ), authors: AuthorMetadata.fromGitRepository(.init(authors: [ .init(name: "Author One"), @@ -84,7 +87,7 @@ extension API.PackageController.GetRoute.Model { history: .init( createdAt: Calendar.current.date(byAdding: .day, value: -70, - to: Current.date())!, + to: now)!, commitCount: 1433, commitCountURL: "https://github.com/Alamofire/Alamofire/commits/main", releaseCount: 79, @@ -96,13 +99,13 @@ extension API.PackageController.GetRoute.Model { .init(name: "lib2", type: .library), .init(name: "exe", type: .executable), .init(name: "lib3", type: .library)], - releases: .init(stable: .init(date: Current.date().adding(days: -12), + releases: .init(stable: .init(date: now.adding(days: -12), link: .init(label: "5.2.0", url: "https://github.com/Alamofire/Alamofire/releases/tag/5.2.0")), - beta: .init(date: Current.date().adding(days: -4), + beta: .init(date: now.adding(days: -4), link: .init(label: "5.3.0-beta.1", url: "https://github.com/Alamofire/Alamofire/releases/tag/5.3.0-beta.1")), - latest: .init(date: Current.date().adding(minutes: -12), + latest: .init(date: now.adding(minutes: -12), link: .init(label: "main", url: "https://github.com/Alamofire/Alamofire"))), dependencies: [ diff --git a/Tests/AppTests/Mocks/AppEnvironment+mock.swift b/Tests/AppTests/Mocks/AppEnvironment+mock.swift index 6e26ff811..46bdd71b0 100644 --- a/Tests/AppTests/Mocks/AppEnvironment+mock.swift +++ b/Tests/AppTests/Mocks/AppEnvironment+mock.swift @@ -38,7 +38,6 @@ extension AppEnvironment { collectionSigningCertificateChain: AppEnvironment.live.collectionSigningCertificateChain, collectionSigningPrivateKey: AppEnvironment.live.collectionSigningPrivateKey, currentReferenceCache: { nil }, - date: { .init() }, dbId: { "db-id" }, environment: { .development }, fetchDocumentation: { _, _ in .init(status: .ok) }, diff --git a/Tests/AppTests/Mocks/BuildMonitorIndex+mock.swift b/Tests/AppTests/Mocks/BuildMonitorIndex+mock.swift index 6f3775a4e..90dd29bb3 100644 --- a/Tests/AppTests/Mocks/BuildMonitorIndex+mock.swift +++ b/Tests/AppTests/Mocks/BuildMonitorIndex+mock.swift @@ -12,14 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -@testable import App import Foundation +@testable import App + +import Dependencies + + extension Array where Element == BuildMonitorIndex.Model { static var mock: [BuildMonitorIndex.Model] { - [ + @Dependency(\.date.now) var now + return [ .init(buildId: .id0, - createdAt: Current.date().adding(hours: -1), + createdAt: now.adding(hours: -1), packageName: "Leftpad", repositoryOwnerName: "Dave Verwer", platform: .macosXcodebuild, @@ -28,7 +33,7 @@ extension Array where Element == BuildMonitorIndex.Model { referenceKind: .release, status: .ok), .init(buildId: .id1, - createdAt: Current.date().adding(hours: -2), + createdAt: now.adding(hours: -2), packageName: "Rester", repositoryOwnerName: "Sven A. Schmidt", platform: .linux, @@ -37,7 +42,7 @@ extension Array where Element == BuildMonitorIndex.Model { referenceKind: .preRelease, status: .failed), .init(buildId: .id2, - createdAt: Current.date().adding(hours: -3), + createdAt: now.adding(hours: -3), packageName: "AccessibilitySnapshotColorBlindness", repositoryOwnerName: "James Sherlock", platform: .linux, diff --git a/Tests/AppTests/Mocks/HomeIndexModel+mock.swift b/Tests/AppTests/Mocks/HomeIndexModel+mock.swift index 9b256d5a3..128ffb9af 100644 --- a/Tests/AppTests/Mocks/HomeIndexModel+mock.swift +++ b/Tests/AppTests/Mocks/HomeIndexModel+mock.swift @@ -14,23 +14,26 @@ @testable import App +import Dependencies + extension HomeIndex.Model { static var mock: HomeIndex.Model { - .init( + @Dependency(\.date.now) var now + return .init( stats: .init(packageCount: 2544), recentPackages: [ - .init(date: Current.date().adding(hours: -2), + .init(date: now.adding(hours: -2), link: .init(label: "Package", url: "https://example.com/package")), - .init(date: Current.date().adding(hours: -2), + .init(date: now.adding(hours: -2), link: .init(label: "Package", url: "https://example.com/package")), - .init(date: Current.date().adding(hours: -2), + .init(date: now.adding(hours: -2), link: .init(label: "Package", url: "https://example.com/package")), - .init(date: Current.date().adding(hours: -2), + .init(date: now.adding(hours: -2), link: .init(label: "Package", url: "https://example.com/package")), - .init(date: Current.date().adding(hours: -2), + .init(date: now.adding(hours: -2), link: .init(label: "Package", url: "https://example.com/package")), - .init(date: Current.date().adding(hours: -2), + .init(date: now.adding(hours: -2), link: .init(label: "Package", url: "https://example.com/package")), ], recentReleases: [ diff --git a/Tests/AppTests/Mocks/MaintainerInfoIndex+mock.swift b/Tests/AppTests/Mocks/MaintainerInfoIndex+mock.swift index ef54102cf..a684e897e 100644 --- a/Tests/AppTests/Mocks/MaintainerInfoIndex+mock.swift +++ b/Tests/AppTests/Mocks/MaintainerInfoIndex+mock.swift @@ -12,9 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Foundation + @testable import App -import Foundation +import Dependencies + extension MaintainerInfoIndex.Model { static var mock: MaintainerInfoIndex.Model { @@ -30,13 +33,14 @@ extension MaintainerInfoIndex.Model { extension Score.Details { static var mock: Self { - .init( + @Dependency(\.date.now) var now + return .init( licenseKind: .compatibleWithAppStore, releaseCount: 10, likeCount: 300, isArchived: false, numberOfDependencies: 3, - lastActivityAt: Current.date().adding(days: -10), + lastActivityAt: now.adding(days: -10), hasDocumentation: true, hasReadme: true, numberOfContributors: 20, diff --git a/Tests/AppTests/PackageCollectionControllerTests.swift b/Tests/AppTests/PackageCollectionControllerTests.swift index e64f3a3b5..6760ca89f 100644 --- a/Tests/AppTests/PackageCollectionControllerTests.swift +++ b/Tests/AppTests/PackageCollectionControllerTests.swift @@ -13,6 +13,8 @@ // limitations under the License. @testable import App + +import Dependencies import SnapshotTesting import XCTVapor @@ -21,53 +23,55 @@ class PackageCollectionControllerTests: AppTestCase { func test_owner_request() async throws { try XCTSkipIf(!isRunningInCI && Current.collectionSigningPrivateKey() == nil, "Skip test for local user due to unset COLLECTION_SIGNING_PRIVATE_KEY env variable") - // setup - Current.date = { .t0 } - let p = try await savePackage(on: app.db, "https://github.com/foo/1") - do { - let v = try Version(id: UUID(), - package: p, - packageName: "P1-main", - reference: .branch("main"), - toolsVersion: "5.0") - try await v.save(on: app.db) - try await Product(version: v, type: .library(.automatic), name: "P1Lib") - .save(on: app.db) - } - do { - let v = try Version(id: UUID(), - package: p, - latest: .release, - packageName: "P1-tag", - reference: .tag(1, 2, 3), - toolsVersion: "5.1") - try await v.save(on: app.db) - try await Product(version: v, type: .library(.automatic), name: "P1Lib", targets: ["t1"]) - .save(on: app.db) - try await Build(version: v, - platform: .iOS, - status: .ok, - swiftVersion: .init(5, 6, 0)).save(on: app.db) - try await Target(version: v, name: "t1").save(on: app.db) - } - try await Repository(package: p, - defaultBranch: "main", - license: .mit, - licenseUrl: "https://foo/mit", - owner: "foo", - summary: "summary 1").create(on: app.db) + try await withDependencies { + $0.date.now = .t0 + } operation: { + let p = try await savePackage(on: app.db, "https://github.com/foo/1") + do { + let v = try Version(id: UUID(), + package: p, + packageName: "P1-main", + reference: .branch("main"), + toolsVersion: "5.0") + try await v.save(on: app.db) + try await Product(version: v, type: .library(.automatic), name: "P1Lib") + .save(on: app.db) + } + do { + let v = try Version(id: UUID(), + package: p, + latest: .release, + packageName: "P1-tag", + reference: .tag(1, 2, 3), + toolsVersion: "5.1") + try await v.save(on: app.db) + try await Product(version: v, type: .library(.automatic), name: "P1Lib", targets: ["t1"]) + .save(on: app.db) + try await Build(version: v, + platform: .iOS, + status: .ok, + swiftVersion: .init(5, 6, 0)).save(on: app.db) + try await Target(version: v, name: "t1").save(on: app.db) + } + try await Repository(package: p, + defaultBranch: "main", + license: .mit, + licenseUrl: "https://foo/mit", + owner: "foo", + summary: "summary 1").create(on: app.db) - // MUT - let encoder = self.encoder - try await app.test( - .GET, - "foo/collection.json", - afterResponse: { @MainActor res async throws in - // validation - XCTAssertEqual(res.status, .ok) - let json = try res.content.decode(PackageCollection.self) - assertSnapshot(of: json, as: .json(encoder)) - }) + // MUT + let encoder = self.encoder + try await app.test( + .GET, + "foo/collection.json", + afterResponse: { @MainActor res async throws in + // validation + XCTAssertEqual(res.status, .ok) + let json = try res.content.decode(PackageCollection.self) + assertSnapshot(of: json, as: .json(encoder)) + }) + } } func test_nonexisting_404() throws { diff --git a/Tests/AppTests/PackageCollectionTests.swift b/Tests/AppTests/PackageCollectionTests.swift index 4bda61fac..562d4353f 100644 --- a/Tests/AppTests/PackageCollectionTests.swift +++ b/Tests/AppTests/PackageCollectionTests.swift @@ -12,13 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -@testable import App -import SnapshotTesting -import Vapor import XCTest +@testable import App + import Basics +import Dependencies import PackageCollectionsSigning +import SnapshotTesting +import Vapor class PackageCollectionTests: AppTestCase { @@ -344,39 +346,42 @@ class PackageCollectionTests: AppTestCase { } func test_generate_from_urls() async throws { - // setup - Current.date = { Date(timeIntervalSince1970: 1610112345) } - let pkg = try await savePackage(on: app.db, "1") - do { - let v = try Version(package: pkg, - latest: .release, - packageName: "package", - reference: .tag(1, 2, 3), - toolsVersion: "5.4") - try await v.save(on: app.db) - try await Product(version: v, type: .library(.automatic), name: "product") - .save(on: app.db) - } - try await Repository(package: pkg, - license: .mit, - licenseUrl: "https://foo/mit", - summary: "summary").create(on: app.db) + try await withDependencies { + $0.date.now = .init(timeIntervalSince1970: 1610112345) + } operation: { + // setup + let pkg = try await savePackage(on: app.db, "1") + do { + let v = try Version(package: pkg, + latest: .release, + packageName: "package", + reference: .tag(1, 2, 3), + toolsVersion: "5.4") + try await v.save(on: app.db) + try await Product(version: v, type: .library(.automatic), name: "product") + .save(on: app.db) + } + try await Repository(package: pkg, + license: .mit, + licenseUrl: "https://foo/mit", + summary: "summary").create(on: app.db) - // MUT - let res = try await PackageCollection.generate(db: self.app.db, - filterBy: .urls(["1"]), - authorName: "Foo", - collectionName: "Foo", - keywords: ["key", "word"], - overview: "overview") + // MUT + let res = try await PackageCollection.generate(db: self.app.db, + filterBy: .urls(["1"]), + authorName: "Foo", + collectionName: "Foo", + keywords: ["key", "word"], + overview: "overview") #if compiler(<6) - await MainActor.run { // validate - assertSnapshot(of: res, as: .json(encoder)) - } + await MainActor.run { // validate + assertSnapshot(of: res, as: .json(encoder)) + } #else - assertSnapshot(of: res, as: .json(encoder)) + assertSnapshot(of: res, as: .json(encoder)) #endif + } } func test_generate_from_urls_noResults() async throws { @@ -397,88 +402,91 @@ class PackageCollectionTests: AppTestCase { } func test_generate_for_owner() async throws { - // setup - Current.date = { Date(timeIntervalSince1970: 1610112345) } - // first package - let p1 = try await savePackage(on: app.db, "https://github.com/foo/1") - do { - let v = try Version(id: UUID(), - package: p1, - packageName: "P1-main", - reference: .branch("main"), - toolsVersion: "5.0") - try await v.save(on: app.db) - try await Product(version: v, type: .library(.automatic), name: "P1Lib") - .save(on: app.db) - } - do { - let v = try Version(id: UUID(), - package: p1, - latest: .release, - packageName: "P1-tag", - reference: .tag(2, 0, 0), - toolsVersion: "5.2") - try await v.save(on: app.db) - try await Product(version: v, type: .library(.automatic), name: "P1Lib", targets: ["t1"]) - .save(on: app.db) - try await Build(version: v, - platform: .iOS, - status: .ok, - swiftVersion: .init(5, 6, 0)).save(on: app.db) - try await Target(version: v, name: "t1").save(on: app.db) - } - // second package - let p2 = try await savePackage(on: app.db, "https://github.com/foo/2") - do { - let v = try Version(id: UUID(), - package: p2, - packageName: "P2-main", - reference: .branch("main"), - toolsVersion: "5.3") - try await v.save(on: app.db) - try await Product(version: v, type: .library(.automatic), name: "P1Lib") - .save(on: app.db) - } - do { - let v = try Version(id: UUID(), - package: p2, - latest: .release, - packageName: "P2-tag", - reference: .tag(1, 2, 3), - toolsVersion: "5.3") - try await v.save(on: app.db) - try await Product(version: v, type: .library(.automatic), name: "P1Lib", targets: ["t2"]) - .save(on: app.db) - try await Target(version: v, name: "t2").save(on: app.db) - } - // unrelated package - _ = try await savePackage(on: app.db, "https://github.com/bar/1") - try await Repository(package: p1, - defaultBranch: "main", - license: .mit, - licenseUrl: "https://foo/mit", - owner: "foo", - summary: "summary 1").create(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - license: .mit, - licenseUrl: "https://foo/mit", - owner: "foo", - summary: "summary 2").create(on: app.db) - - // MUT - let res = try await PackageCollection.generate(db: self.app.db, - filterBy: .author("foo"), - authorName: "Foo", - keywords: ["key", "word"]) - + try await withDependencies { + $0.date.now = .init(timeIntervalSince1970: 1610112345) + } operation: { + // setup + // first package + let p1 = try await savePackage(on: app.db, "https://github.com/foo/1") + do { + let v = try Version(id: UUID(), + package: p1, + packageName: "P1-main", + reference: .branch("main"), + toolsVersion: "5.0") + try await v.save(on: app.db) + try await Product(version: v, type: .library(.automatic), name: "P1Lib") + .save(on: app.db) + } + do { + let v = try Version(id: UUID(), + package: p1, + latest: .release, + packageName: "P1-tag", + reference: .tag(2, 0, 0), + toolsVersion: "5.2") + try await v.save(on: app.db) + try await Product(version: v, type: .library(.automatic), name: "P1Lib", targets: ["t1"]) + .save(on: app.db) + try await Build(version: v, + platform: .iOS, + status: .ok, + swiftVersion: .init(5, 6, 0)).save(on: app.db) + try await Target(version: v, name: "t1").save(on: app.db) + } + // second package + let p2 = try await savePackage(on: app.db, "https://github.com/foo/2") + do { + let v = try Version(id: UUID(), + package: p2, + packageName: "P2-main", + reference: .branch("main"), + toolsVersion: "5.3") + try await v.save(on: app.db) + try await Product(version: v, type: .library(.automatic), name: "P1Lib") + .save(on: app.db) + } + do { + let v = try Version(id: UUID(), + package: p2, + latest: .release, + packageName: "P2-tag", + reference: .tag(1, 2, 3), + toolsVersion: "5.3") + try await v.save(on: app.db) + try await Product(version: v, type: .library(.automatic), name: "P1Lib", targets: ["t2"]) + .save(on: app.db) + try await Target(version: v, name: "t2").save(on: app.db) + } + // unrelated package + _ = try await savePackage(on: app.db, "https://github.com/bar/1") + try await Repository(package: p1, + defaultBranch: "main", + license: .mit, + licenseUrl: "https://foo/mit", + owner: "foo", + summary: "summary 1").create(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + license: .mit, + licenseUrl: "https://foo/mit", + owner: "foo", + summary: "summary 2").create(on: app.db) + + // MUT + let res = try await PackageCollection.generate(db: self.app.db, + filterBy: .author("foo"), + authorName: "Foo", + keywords: ["key", "word"]) + #if compiler(<6) - await MainActor.run { // validate - assertSnapshot(of: res, as: .json(encoder)) - } + await MainActor.run { // validate + assertSnapshot(of: res, as: .json(encoder)) + } #else - assertSnapshot(of: res, as: .json(encoder)) + assertSnapshot(of: res, as: .json(encoder)) #endif + } } func test_generate_for_owner_noResults() async throws { @@ -569,18 +577,22 @@ class PackageCollectionTests: AppTestCase { try await Target(version: v, name: "t1").save(on: app.db) } - // MUT - let res = try await PackageCollection.generate(db: self.app.db, - filterBy: .author("foo"), - authorName: "Foo", - collectionName: "Foo", - keywords: ["key", "word"], - overview: "overview") - - // validate - XCTAssertEqual(res.packages.count, 1) - XCTAssertEqual(res.packages.flatMap { $0.versions.map({$0.version}) }, - ["2.0.0-b1", "1.2.3"]) + try await withDependencies { + $0.date.now = .now + } operation: { + // MUT + let res = try await PackageCollection.generate(db: self.app.db, + filterBy: .author("foo"), + authorName: "Foo", + collectionName: "Foo", + keywords: ["key", "word"], + overview: "overview") + + // validate + XCTAssertEqual(res.packages.count, 1) + XCTAssertEqual(res.packages.flatMap { $0.versions.map({$0.version}) }, + ["2.0.0-b1", "1.2.3"]) + } } func test_require_products() async throws { @@ -661,14 +673,18 @@ class PackageCollectionTests: AppTestCase { owner: "Foo", summary: "summary 1").create(on: app.db) - // MUT - let res = try await PackageCollection.generate(db: self.app.db, - // looking for owner "foo" - filterBy: .author("foo"), - collectionName: "collection") + try await withDependencies { + $0.date.now = .now + } operation: { + // MUT + let res = try await PackageCollection.generate(db: self.app.db, + // looking for owner "foo" + filterBy: .author("foo"), + collectionName: "collection") - // validate - XCTAssertEqual(res.packages.count, 1) + // validate + XCTAssertEqual(res.packages.count, 1) + } } func test_generate_ownerName() async throws { @@ -701,15 +717,19 @@ class PackageCollectionTests: AppTestCase { ownerName: "Foo Org", summary: "summary 1").create(on: app.db) - // MUT - let res = try await PackageCollection.generate(db: self.app.db, - filterBy: .author("foo"), - authorName: "Foo", - keywords: ["key", "word"]) - - // validate - XCTAssertEqual(res.name, "Packages by Foo Org") - XCTAssertEqual(res.overview, "A collection of packages authored by Foo Org from the Swift Package Index") + try await withDependencies { + $0.date.now = .now + } operation: { + // MUT + let res = try await PackageCollection.generate(db: self.app.db, + filterBy: .author("foo"), + authorName: "Foo", + keywords: ["key", "word"]) + + // validate + XCTAssertEqual(res.name, "Packages by Foo Org") + XCTAssertEqual(res.overview, "A collection of packages authored by Foo Org from the Swift Package Index") + } } func test_Compatibility() throws { diff --git a/Tests/AppTests/PackageController+routesTests.swift b/Tests/AppTests/PackageController+routesTests.swift index 638863ea3..c06db10ba 100644 --- a/Tests/AppTests/PackageController+routesTests.swift +++ b/Tests/AppTests/PackageController+routesTests.swift @@ -16,6 +16,7 @@ import XCTest @testable import App +import Dependencies import SnapshotTesting import SwiftSoup import Vapor @@ -1347,7 +1348,7 @@ class PackageController_routesTests: SnapshotTestCase { let package = Package(url: URL(stringLiteral: "https://example.com/owner/repo0")) try await package.save(on: app.db) try await Repository(package: package, defaultBranch: "default", - lastCommitDate: Current.date(), + lastCommitDate: Date.now, name: "Repo0", owner: "Owner").save(on: app.db) try await Version(package: package, latest: .defaultBranch, packageName: "SomePackage", reference: .branch("default")).save(on: app.db) @@ -1371,7 +1372,7 @@ class PackageController_routesTests: SnapshotTestCase { let package = Package(url: URL(stringLiteral: "https://example.com/owner/repo0")) try await package.save(on: app.db) try await Repository(package: package, defaultBranch: "default", - lastCommitDate: Current.date(), + lastCommitDate: Date.now, name: "Repo0", owner: "Owner").save(on: app.db) try await Version(package: package, latest: .defaultBranch, packageName: "SomePackage", reference: .branch("default")).save(on: app.db) @@ -1419,28 +1420,31 @@ class PackageController_routesTests: SnapshotTestCase { return "" } // Make sure the new commit doesn't get throttled - Current.date = { .t1 + Constants.branchVersionRefreshDelay + 1 } - Current.fetchDocumentation = { _, _ in .init(status: .ok, body: .mockIndexHTML()) } - - // Ensure documentation is resolved - try await app.test(.GET, "/foo/bar/~/documentation/target") { @MainActor res in - await Task.yield() // essential to avoid deadlocking - XCTAssertEqual(res.status, .ok) - assertSnapshot(of: String(buffer: res.body), as: .html, named: "index") - } + try await withDependencies { + $0.date.now = .t1 + Constants.branchVersionRefreshDelay + 1 + } operation: { + Current.fetchDocumentation = { _, _ in .init(status: .ok, body: .mockIndexHTML()) } + + // Ensure documentation is resolved + try await app.test(.GET, "/foo/bar/~/documentation/target") { @MainActor res in + await Task.yield() // essential to avoid deadlocking + XCTAssertEqual(res.status, .ok) + assertSnapshot(of: String(buffer: res.body), as: .html, named: "index") + } - // Run analyze to detect a new default branch version - try await Analyze.analyze(client: app.client, database: app.db, mode: .limit(1)) + // Run analyze to detect a new default branch version + try await Analyze.analyze(client: app.client, database: app.db, mode: .limit(1)) - // Confirm that analysis has picked up the new version - let commit = try await Version.query(on: app.db).all().map(\.commit) - XCTAssertEqual(commit, ["new-commit"]) + // Confirm that analysis has picked up the new version + let commit = try await Version.query(on: app.db).all().map(\.commit) + XCTAssertEqual(commit, ["new-commit"]) - // Ensure documentation is still being resolved - try await app.test(.GET, "/foo/bar/~/documentation/target") { @MainActor res in - await Task.yield() // essential to avoid deadlocking - XCTAssertEqual(res.status, .ok) - assertSnapshot(of: String(buffer: res.body), as: .html, named: "index") + // Ensure documentation is still being resolved + try await app.test(.GET, "/foo/bar/~/documentation/target") { @MainActor res in + await Task.yield() // essential to avoid deadlocking + XCTAssertEqual(res.status, .ok) + assertSnapshot(of: String(buffer: res.body), as: .html, named: "index") + } } } diff --git a/Tests/AppTests/PackageReleasesModelTests.swift b/Tests/AppTests/PackageReleasesModelTests.swift index 48540fa69..d91b09b7d 100644 --- a/Tests/AppTests/PackageReleasesModelTests.swift +++ b/Tests/AppTests/PackageReleasesModelTests.swift @@ -15,6 +15,7 @@ @testable import App import XCTVapor +import Dependencies class PackageReleasesModelTests: AppTestCase { @@ -32,33 +33,36 @@ class PackageReleasesModelTests: AppTestCase { NSTimeZone.default = oldDefault } - Current.date = { .spiBirthday } - let pkg = Package(id: UUID(), url: "1".asGithubUrl.url) - try await pkg.save(on: app.db) + try await withDependencies { + $0.date.now = .spiBirthday + } operation: { + let pkg = Package(id: UUID(), url: "1".asGithubUrl.url) + try await pkg.save(on: app.db) - try await Repository(package: pkg, releases: [ - .mock(description: "Release Notes", descriptionHTML: "Release Notes", - publishedAt: 2, tagName: "1.0.0", url: "some url"), + try await Repository(package: pkg, releases: [ + .mock(description: "Release Notes", descriptionHTML: "Release Notes", + publishedAt: 2, tagName: "1.0.0", url: "some url"), - .mock(description: nil, descriptionHTML: nil, - publishedAt: 1, tagName: "0.0.1", url: "some url"), - ]).save(on: app.db) - let jpr = try await Package.fetchCandidate(app.db, id: pkg.id!) + .mock(description: nil, descriptionHTML: nil, + publishedAt: 1, tagName: "0.0.1", url: "some url"), + ]).save(on: app.db) + let jpr = try await Package.fetchCandidate(app.db, id: pkg.id!) - // MUT - let model = try XCTUnwrap(PackageReleases.Model(package: jpr)) + // MUT + let model = try XCTUnwrap(PackageReleases.Model(package: jpr)) - // Validate - XCTAssertEqual(model.releases, [ - .init(title: "1.0.0", date: "Released 50 years ago on 1 January 1970", - html: "Release Notes", link: "some url"), + // Validate + XCTAssertEqual(model.releases, [ + .init(title: "1.0.0", date: "Released 50 years ago on 1 January 1970", + html: "Release Notes", link: "some url"), - .init(title: "0.0.1", date: "Released 50 years ago on 1 January 1970", - html: nil, link: "some url"), - ]) - // NOTE(heckj): test is sensitive to local time zones, breaks when run at GMT-7 - // resolves as `31 December 1969` + .init(title: "0.0.1", date: "Released 50 years ago on 1 January 1970", + html: nil, link: "some url"), + ]) + // NOTE(heckj): test is sensitive to local time zones, breaks when run at GMT-7 + // resolves as `31 December 1969` + } } func test_dateFormatting() throws { diff --git a/Tests/AppTests/PackageTests.swift b/Tests/AppTests/PackageTests.swift index 754d1bd5f..f7cadab63 100644 --- a/Tests/AppTests/PackageTests.swift +++ b/Tests/AppTests/PackageTests.swift @@ -14,6 +14,7 @@ @testable import App +import Dependencies import Fluent import SQLKit import Vapor @@ -77,12 +78,16 @@ final class PackageTests: AppTestCase { } func test_save_scoreDetails() async throws { - let pkg = Package(url: "1") - let scoreDetails = Score.Details.mock - pkg.scoreDetails = scoreDetails - try await pkg.save(on: app.db) - let readBack = try await XCTUnwrapAsync(try await Package.query(on: app.db).first()) - XCTAssertEqual(readBack.scoreDetails, scoreDetails) + try await withDependencies { + $0.date.now = .now + } operation: { + let pkg = Package(url: "1") + let scoreDetails = Score.Details.mock + pkg.scoreDetails = scoreDetails + try await pkg.save(on: app.db) + let readBack = try await XCTUnwrapAsync(try await Package.query(on: app.db).first()) + XCTAssertEqual(readBack.scoreDetails, scoreDetails) + } } func test_encode() throws { @@ -165,12 +170,12 @@ final class PackageTests: AppTestCase { try await Repository(package: pkg, defaultBranch: "default").create(on: app.db) let versions = [ try Version(package: pkg, reference: .branch("branch")), - try Version(package: pkg, commitDate: Current.date().adding(days: -1), + try Version(package: pkg, commitDate: Date.now.adding(days: -1), reference: .branch("default")), try Version(package: pkg, reference: .tag(.init(1, 2, 3))), - try Version(package: pkg, commitDate: Current.date().adding(days: -3), + try Version(package: pkg, commitDate: Date.now.adding(days: -3), reference: .tag(.init(2, 1, 0))), - try Version(package: pkg, commitDate: Current.date().adding(days: -2), + try Version(package: pkg, commitDate: Date.now.adding(days: -2), reference: .tag(.init(3, 0, 0, "beta"))), ] try await versions.create(on: app.db) @@ -282,83 +287,88 @@ final class PackageTests: AppTestCase { } func test_isNew() async throws { - // setup - let url = "1".asGithubUrl - Current.fetchMetadata = { _, owner, repository in .mock(owner: owner, repository: repository) } - Current.fetchPackageList = { _ in [url.url] } - Current.git.commitCount = { @Sendable _ in 12 } - Current.git.firstCommitDate = { @Sendable _ in Date(timeIntervalSince1970: 0) } - Current.git.getTags = { @Sendable _ in [] } - Current.git.hasBranch = { @Sendable _, _ in true } - Current.git.lastCommitDate = { @Sendable _ in Date(timeIntervalSince1970: 1) } - Current.git.revisionInfo = { @Sendable _, _ in - .init(commit: "sha", - date: Date(timeIntervalSince1970: 0)) - } - Current.git.shortlog = { @Sendable _ in + try await withDependencies { + $0.date.now = .now + } operation: { + // setup + let url = "1".asGithubUrl + Current.fetchMetadata = { _, owner, repository in .mock(owner: owner, repository: repository) } + Current.fetchPackageList = { _ in [url.url] } + Current.git.commitCount = { @Sendable _ in 12 } + Current.git.firstCommitDate = { @Sendable _ in Date(timeIntervalSince1970: 0) } + Current.git.getTags = { @Sendable _ in [] } + Current.git.hasBranch = { @Sendable _, _ in true } + Current.git.lastCommitDate = { @Sendable _ in Date(timeIntervalSince1970: 1) } + Current.git.revisionInfo = { @Sendable _, _ in + .init(commit: "sha", + date: Date(timeIntervalSince1970: 0)) + } + Current.git.shortlog = { @Sendable _ in """ 10\tPerson 1 2\tPerson 2 """ - } - Current.shell.run = { @Sendable cmd, path in - if cmd.description.hasSuffix("swift package dump-package") { - return #"{ "name": "Mock", "products": [] }"# } - return "" - } - let db = app.db - // run reconcile to ingest package - try await reconcile(client: app.client, database: app.db) - try await XCTAssertEqualAsync(try await Package.query(on: db).count(), 1) - - // MUT & validate - do { - let pkg = try await XCTUnwrapAsync(try await Package.query(on: app.db).first()) - XCTAssertTrue(pkg.isNew) - } - - // run ingestion to progress package through pipeline - try await ingest(client: app.client, database: app.db, mode: .limit(10)) - - // MUT & validate - do { - let pkg = try await XCTUnwrapAsync(try await Package.query(on: app.db).first()) - XCTAssertTrue(pkg.isNew) - } - - // run analysis to progress package through pipeline - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) - - // MUT & validate - do { - let pkg = try await XCTUnwrapAsync(try await Package.query(on: app.db).first()) - XCTAssertFalse(pkg.isNew) - } - - // run stages again to simulate the cycle... - - try await reconcile(client: app.client, database: app.db) - do { - let pkg = try await XCTUnwrapAsync(try await Package.query(on: app.db).first()) - XCTAssertFalse(pkg.isNew) - } - - Current.date = { Date().addingTimeInterval(Constants.reIngestionDeadtime) } - try await ingest(client: app.client, database: app.db, mode: .limit(10)) - do { - let pkg = try await XCTUnwrapAsync(try await Package.query(on: app.db).first()) - XCTAssertFalse(pkg.isNew) - } - - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) - do { - let pkg = try await XCTUnwrapAsync(try await Package.query(on: app.db).first()) - XCTAssertFalse(pkg.isNew) + Current.shell.run = { @Sendable cmd, path in + if cmd.description.hasSuffix("swift package dump-package") { + return #"{ "name": "Mock", "products": [] }"# + } + return "" + } + let db = app.db + // run reconcile to ingest package + try await reconcile(client: app.client, database: app.db) + try await XCTAssertEqualAsync(try await Package.query(on: db).count(), 1) + + // MUT & validate + do { + let pkg = try await XCTUnwrapAsync(try await Package.query(on: app.db).first()) + XCTAssertTrue(pkg.isNew) + } + + // run ingestion to progress package through pipeline + try await ingest(client: app.client, database: app.db, mode: .limit(10)) + + // MUT & validate + do { + let pkg = try await XCTUnwrapAsync(try await Package.query(on: app.db).first()) + XCTAssertTrue(pkg.isNew) + } + + // run analysis to progress package through pipeline + try await Analyze.analyze(client: app.client, database: app.db, mode: .limit(10)) + + // MUT & validate + do { + let pkg = try await XCTUnwrapAsync(try await Package.query(on: app.db).first()) + XCTAssertFalse(pkg.isNew) + } + + // run stages again to simulate the cycle... + + try await reconcile(client: app.client, database: app.db) + do { + let pkg = try await XCTUnwrapAsync(try await Package.query(on: app.db).first()) + XCTAssertFalse(pkg.isNew) + } + + try await withDependencies { + $0.date.now = .now.addingTimeInterval(Constants.reIngestionDeadtime) + } operation: { + try await ingest(client: app.client, database: app.db, mode: .limit(10)) + + do { + let pkg = try await XCTUnwrapAsync(try await Package.query(on: app.db).first()) + XCTAssertFalse(pkg.isNew) + } + + try await Analyze.analyze(client: app.client, database: app.db, mode: .limit(10)) + + do { + let pkg = try await XCTUnwrapAsync(try await Package.query(on: app.db).first()) + XCTAssertFalse(pkg.isNew) + } + } } } diff --git a/Tests/AppTests/PipelineTests.swift b/Tests/AppTests/PipelineTests.swift index 93aa1779d..80efbb85b 100644 --- a/Tests/AppTests/PipelineTests.swift +++ b/Tests/AppTests/PipelineTests.swift @@ -12,11 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +import XCTest + @testable import App +import Dependencies import SQLKit import Vapor -import XCTest // Tests concerning the full pipeline of operations: @@ -31,10 +33,14 @@ class PipelineTests: AppTestCase { Package(url: "1", status: .ok, processingStage: .reconciliation), Package(url: "2", status: .ok, processingStage: .reconciliation), ].save(on: app.db) - // fast forward our clock by the deadtime interval - Current.date = { Date().addingTimeInterval(Constants.reIngestionDeadtime) } - let batch = try await Package.fetchCandidates(app.db, for: .ingestion, limit: 10) - XCTAssertEqual(batch.map(\.model.url), ["1", "2"]) + + try await withDependencies { + // fast forward our clock by the deadtime interval + $0.date.now = .now.addingTimeInterval(Constants.reIngestionDeadtime) + } operation: { + let batch = try await Package.fetchCandidates(app.db, for: .ingestion, limit: 10) + XCTAssertEqual(batch.map(\.model.url), ["1", "2"]) + } } func test_fetchCandidates_ingestion_limit() async throws { @@ -42,10 +48,14 @@ class PipelineTests: AppTestCase { Package(url: "1", status: .ok, processingStage: .reconciliation), Package(url: "2", status: .ok, processingStage: .reconciliation), ].save(on: app.db) - // fast forward our clock by the deadtime interval - Current.date = { Date().addingTimeInterval(Constants.reIngestionDeadtime) } - let batch = try await Package.fetchCandidates(app.db, for: .ingestion, limit: 1) - XCTAssertEqual(batch.map(\.model.url), ["1"]) + + try await withDependencies { + // fast forward our clock by the deadtime interval + $0.date.now = .now.addingTimeInterval(Constants.reIngestionDeadtime) + } operation: { + let batch = try await Package.fetchCandidates(app.db, for: .ingestion, limit: 1) + XCTAssertEqual(batch.map(\.model.url), ["1"]) + } } func test_fetchCandidates_ingestion_correct_stage() async throws { @@ -55,8 +65,13 @@ class PipelineTests: AppTestCase { Package(url: "2", status: .ok, processingStage: .reconciliation), Package(url: "3", status: .ok, processingStage: .analysis), ].save(on: app.db) - let batch = try await Package.fetchCandidates(app.db, for: .ingestion, limit: 10) - XCTAssertEqual(batch.map(\.model.url), ["2"]) + + try await withDependencies { + $0.date.now = .now + } operation: { + let batch = try await Package.fetchCandidates(app.db, for: .ingestion, limit: 10) + XCTAssertEqual(batch.map(\.model.url), ["2"]) + } } func test_fetchCandidates_ingestion_prefer_new() async throws { @@ -66,10 +81,14 @@ class PipelineTests: AppTestCase { Package(url: "2", status: .new, processingStage: .reconciliation), Package(url: "3", status: .ok, processingStage: .reconciliation), ].save(on: app.db) - // fast forward our clock by the deadtime interval - Current.date = { Date().addingTimeInterval(Constants.reIngestionDeadtime) } - let batch = try await Package.fetchCandidates(app.db, for: .ingestion, limit: 10) - XCTAssertEqual(batch.map(\.model.url), ["2", "1", "3"]) + + try await withDependencies { + // fast forward our clock by the deadtime interval + $0.date.now = .now.addingTimeInterval(Constants.reIngestionDeadtime) + } operation: { + let batch = try await Package.fetchCandidates(app.db, for: .ingestion, limit: 10) + XCTAssertEqual(batch.map(\.model.url), ["2", "1", "3"]) + } } func test_fetchCandidates_ingestion_eventual_refresh() async throws { @@ -83,8 +102,13 @@ class PipelineTests: AppTestCase { try await (app.db as! SQLDatabase).raw( "update packages set updated_at = updated_at - interval '91 mins' where id = \(bind: p2.id)" ).run() - let batch = try await Package.fetchCandidates(app.db, for: .ingestion, limit: 10) - XCTAssertEqual(batch.map(\.model.url), ["2"]) + + try await withDependencies { + $0.date.now = .now + } operation: { + let batch = try await Package.fetchCandidates(app.db, for: .ingestion, limit: 10) + XCTAssertEqual(batch.map(\.model.url), ["2"]) + } } func test_fetchCandidates_ingestion_refresh_analysis_only() async throws { @@ -99,10 +123,14 @@ class PipelineTests: AppTestCase { Package(url: "2", status: .new, processingStage: .ingestion), Package(url: "3", status: .new, processingStage: .analysis), ].save(on: app.db) - // fast forward our clock by the deadtime interval - Current.date = { Date().addingTimeInterval(Constants.reIngestionDeadtime) } - let batch = try await Package.fetchCandidates(app.db, for: .ingestion, limit: 10) - XCTAssertEqual(batch.map(\.model.url), ["1", "3"]) + + try await withDependencies { + // fast forward our clock by the deadtime interval + $0.date.now = .now.addingTimeInterval(Constants.reIngestionDeadtime) + } operation: { + let batch = try await Package.fetchCandidates(app.db, for: .ingestion, limit: 10) + XCTAssertEqual(batch.map(\.model.url), ["1", "3"]) + } } func test_fetchCandidates_analysis_correct_stage() async throws { @@ -130,136 +158,142 @@ class PipelineTests: AppTestCase { } func test_processing_pipeline() async throws { - // Test pipeline pick-up end to end - // setup - let urls = ["1", "2", "3"].asGithubUrls - Current.fetchMetadata = { _, owner, repository in .mock(owner: owner, repository: repository) } - Current.fetchPackageList = { _ in urls.asURLs } - - Current.git.commitCount = { @Sendable _ in 12 } - Current.git.firstCommitDate = { @Sendable _ in .t0 } - Current.git.lastCommitDate = { @Sendable _ in .t1 } - Current.git.getTags = { @Sendable _ in [] } - Current.git.hasBranch = { @Sendable _, _ in true } - Current.git.revisionInfo = { @Sendable _, _ in .init(commit: "sha", date: .t0) } - Current.git.shortlog = { @Sendable _ in + try await withDependencies { + $0.date.now = .now + } operation: { + // Test pipeline pick-up end to end + // setup + let urls = ["1", "2", "3"].asGithubUrls + Current.fetchMetadata = { _, owner, repository in .mock(owner: owner, repository: repository) } + Current.fetchPackageList = { _ in urls.asURLs } + + Current.git.commitCount = { @Sendable _ in 12 } + Current.git.firstCommitDate = { @Sendable _ in .t0 } + Current.git.lastCommitDate = { @Sendable _ in .t1 } + Current.git.getTags = { @Sendable _ in [] } + Current.git.hasBranch = { @Sendable _, _ in true } + Current.git.revisionInfo = { @Sendable _, _ in .init(commit: "sha", date: .t0) } + Current.git.shortlog = { @Sendable _ in """ 10\tPerson 1 2\tPerson 2 """ - } - - Current.shell.run = { @Sendable cmd, path in - if cmd.description.hasSuffix("swift package dump-package") { - return #"{ "name": "Mock", "products": [], "targets": [] }"# } - return "" - } - - // MUT - first stage - try await reconcile(client: app.client, database: app.db) - - do { // validate - let packages = try await Package.query(on: app.db).sort(\.$url).all() - XCTAssertEqual(packages.map(\.url), ["1", "2", "3"].asGithubUrls) - XCTAssertEqual(packages.map(\.status), [.new, .new, .new]) - XCTAssertEqual(packages.map(\.processingStage), [.reconciliation, .reconciliation, .reconciliation]) - XCTAssertEqual(packages.map(\.isNew), [true, true, true]) - } - - // MUT - second stage - try await ingest(client: app.client, database: app.db, mode: .limit(10)) - - do { // validate - let packages = try await Package.query(on: app.db).sort(\.$url).all() - XCTAssertEqual(packages.map(\.url), ["1", "2", "3"].asGithubUrls) - XCTAssertEqual(packages.map(\.status), [.new, .new, .new]) - XCTAssertEqual(packages.map(\.processingStage), [.ingestion, .ingestion, .ingestion]) - XCTAssertEqual(packages.map(\.isNew), [true, true, true]) - } - - // MUT - third stage - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) - - do { // validate - let packages = try await Package.query(on: app.db).sort(\.$url).all() - XCTAssertEqual(packages.map(\.url), ["1", "2", "3"].asGithubUrls) - XCTAssertEqual(packages.map(\.status), [.ok, .ok, .ok]) - XCTAssertEqual(packages.map(\.processingStage), [.analysis, .analysis, .analysis]) - XCTAssertEqual(packages.map(\.isNew), [false, false, false]) - } - - // Now we've got a new package and a deletion - Current.fetchPackageList = { _ in ["1", "3", "4"].asGithubUrls.asURLs } - - // MUT - reconcile again - try await reconcile(client: app.client, database: app.db) - - do { // validate - only new package moves to .reconciliation stage - let packages = try await Package.query(on: app.db).sort(\.$url).all() - XCTAssertEqual(packages.map(\.url), ["1", "3", "4"].asGithubUrls) - XCTAssertEqual(packages.map(\.status), [.ok, .ok, .new]) - XCTAssertEqual(packages.map(\.processingStage), [.analysis, .analysis, .reconciliation]) - XCTAssertEqual(packages.map(\.isNew), [false, false, true]) - } - - // MUT - ingest again - try await ingest(client: app.client, database: app.db, mode: .limit(10)) - - do { // validate - only new package moves to .ingestion stage - let packages = try await Package.query(on: app.db).sort(\.$url).all() - XCTAssertEqual(packages.map(\.url), ["1", "3", "4"].asGithubUrls) - XCTAssertEqual(packages.map(\.status), [.ok, .ok, .new]) - XCTAssertEqual(packages.map(\.processingStage), [.analysis, .analysis, .ingestion]) - XCTAssertEqual(packages.map(\.isNew), [false, false, true]) - } - - // MUT - analyze again - let lastAnalysis = Current.date() - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) - - do { // validate - only new package moves to .ingestion stage - let packages = try await Package.query(on: app.db).sort(\.$url).all() - XCTAssertEqual(packages.map(\.url), ["1", "3", "4"].asGithubUrls) - XCTAssertEqual(packages.map(\.status), [.ok, .ok, .ok]) - XCTAssertEqual(packages.map(\.processingStage), [.analysis, .analysis, .analysis]) - XCTAssertEqual(packages.map { $0.updatedAt! > lastAnalysis }, [false, false, true]) - XCTAssertEqual(packages.map(\.isNew), [false, false, false]) - } - - // fast forward our clock by the deadtime interval - Current.date = { Date().addingTimeInterval(Constants.reIngestionDeadtime) } - - // MUT - ingest yet again - try await ingest(client: app.client, database: app.db, mode: .limit(10)) - - do { // validate - now all three packages should have been updated - let packages = try await Package.query(on: app.db).sort(\.$url).all() - XCTAssertEqual(packages.map(\.url), ["1", "3", "4"].asGithubUrls) - XCTAssertEqual(packages.map(\.status), [.ok, .ok, .ok]) - XCTAssertEqual(packages.map(\.processingStage), [.ingestion, .ingestion, .ingestion]) - XCTAssertEqual(packages.map(\.isNew), [false, false, false]) - } - - // MUT - re-run analysis to complete the sequence - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) - - do { // validate - only new package moves to .ingestion stage - let packages = try await Package.query(on: app.db).sort(\.$url).all() - XCTAssertEqual(packages.map(\.url), ["1", "3", "4"].asGithubUrls) - XCTAssertEqual(packages.map(\.status), [.ok, .ok, .ok]) - XCTAssertEqual(packages.map(\.processingStage), [.analysis, .analysis, .analysis]) - XCTAssertEqual(packages.map(\.isNew), [false, false, false]) + + Current.shell.run = { @Sendable cmd, path in + if cmd.description.hasSuffix("swift package dump-package") { + return #"{ "name": "Mock", "products": [], "targets": [] }"# + } + return "" + } + + // MUT - first stage + try await reconcile(client: app.client, database: app.db) + + do { // validate + let packages = try await Package.query(on: app.db).sort(\.$url).all() + XCTAssertEqual(packages.map(\.url), ["1", "2", "3"].asGithubUrls) + XCTAssertEqual(packages.map(\.status), [.new, .new, .new]) + XCTAssertEqual(packages.map(\.processingStage), [.reconciliation, .reconciliation, .reconciliation]) + XCTAssertEqual(packages.map(\.isNew), [true, true, true]) + } + + // MUT - second stage + try await ingest(client: app.client, database: app.db, mode: .limit(10)) + + do { // validate + let packages = try await Package.query(on: app.db).sort(\.$url).all() + XCTAssertEqual(packages.map(\.url), ["1", "2", "3"].asGithubUrls) + XCTAssertEqual(packages.map(\.status), [.new, .new, .new]) + XCTAssertEqual(packages.map(\.processingStage), [.ingestion, .ingestion, .ingestion]) + XCTAssertEqual(packages.map(\.isNew), [true, true, true]) + } + + // MUT - third stage + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) + + do { // validate + let packages = try await Package.query(on: app.db).sort(\.$url).all() + XCTAssertEqual(packages.map(\.url), ["1", "2", "3"].asGithubUrls) + XCTAssertEqual(packages.map(\.status), [.ok, .ok, .ok]) + XCTAssertEqual(packages.map(\.processingStage), [.analysis, .analysis, .analysis]) + XCTAssertEqual(packages.map(\.isNew), [false, false, false]) + } + + // Now we've got a new package and a deletion + Current.fetchPackageList = { _ in ["1", "3", "4"].asGithubUrls.asURLs } + + // MUT - reconcile again + try await reconcile(client: app.client, database: app.db) + + do { // validate - only new package moves to .reconciliation stage + let packages = try await Package.query(on: app.db).sort(\.$url).all() + XCTAssertEqual(packages.map(\.url), ["1", "3", "4"].asGithubUrls) + XCTAssertEqual(packages.map(\.status), [.ok, .ok, .new]) + XCTAssertEqual(packages.map(\.processingStage), [.analysis, .analysis, .reconciliation]) + XCTAssertEqual(packages.map(\.isNew), [false, false, true]) + } + + // MUT - ingest again + try await ingest(client: app.client, database: app.db, mode: .limit(10)) + + do { // validate - only new package moves to .ingestion stage + let packages = try await Package.query(on: app.db).sort(\.$url).all() + XCTAssertEqual(packages.map(\.url), ["1", "3", "4"].asGithubUrls) + XCTAssertEqual(packages.map(\.status), [.ok, .ok, .new]) + XCTAssertEqual(packages.map(\.processingStage), [.analysis, .analysis, .ingestion]) + XCTAssertEqual(packages.map(\.isNew), [false, false, true]) + } + + // MUT - analyze again + let lastAnalysis = Date.now + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) + + do { // validate - only new package moves to .ingestion stage + let packages = try await Package.query(on: app.db).sort(\.$url).all() + XCTAssertEqual(packages.map(\.url), ["1", "3", "4"].asGithubUrls) + XCTAssertEqual(packages.map(\.status), [.ok, .ok, .ok]) + XCTAssertEqual(packages.map(\.processingStage), [.analysis, .analysis, .analysis]) + XCTAssertEqual(packages.map { $0.updatedAt! > lastAnalysis }, [false, false, true]) + XCTAssertEqual(packages.map(\.isNew), [false, false, false]) + } + + try await withDependencies { + // fast forward our clock by the deadtime interval + $0.date.now = .now.addingTimeInterval(Constants.reIngestionDeadtime) + } operation: { + // MUT - ingest yet again + try await ingest(client: app.client, database: app.db, mode: .limit(10)) + + do { // validate - now all three packages should have been updated + let packages = try await Package.query(on: app.db).sort(\.$url).all() + XCTAssertEqual(packages.map(\.url), ["1", "3", "4"].asGithubUrls) + XCTAssertEqual(packages.map(\.status), [.ok, .ok, .ok]) + XCTAssertEqual(packages.map(\.processingStage), [.ingestion, .ingestion, .ingestion]) + XCTAssertEqual(packages.map(\.isNew), [false, false, false]) + } + + // MUT - re-run analysis to complete the sequence + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) + + do { // validate - only new package moves to .ingestion stage + let packages = try await Package.query(on: app.db).sort(\.$url).all() + XCTAssertEqual(packages.map(\.url), ["1", "3", "4"].asGithubUrls) + XCTAssertEqual(packages.map(\.status), [.ok, .ok, .ok]) + XCTAssertEqual(packages.map(\.processingStage), [.analysis, .analysis, .analysis]) + XCTAssertEqual(packages.map(\.isNew), [false, false, false]) + } + + // at this point we've ensured that retriggering ingestion after the deadtime will + // refresh analysis as expected + } } - - // at this point we've ensured that retriggering ingestion after the deadtime will - // refresh analysis as expected } } diff --git a/Tests/AppTests/ReAnalyzeVersionsTests.swift b/Tests/AppTests/ReAnalyzeVersionsTests.swift index eb78cfe67..118c4c484 100644 --- a/Tests/AppTests/ReAnalyzeVersionsTests.swift +++ b/Tests/AppTests/ReAnalyzeVersionsTests.swift @@ -12,77 +12,82 @@ // See the License for the specific language governing permissions and // limitations under the License. +import XCTest + @testable import App +import Dependencies import Fluent import SQLKit import Vapor -import XCTest class ReAnalyzeVersionsTests: AppTestCase { func test_reAnalyzeVersions() async throws { // Basic end-to-end test - // setup - // - package dump does not include toolsVersion, targets to simulate an "old version" - // - run analysis to create existing version - // - validate that initial state is reflected - // - then change input data in fields that are affecting existing versions (which `analysis` is "blind" to) - // - run analysis again to confirm "blindness" - // - run re-analysis and confirm changes are now reflected - let pkg = try await savePackage(on: app.db, - "https://github.com/foo/1".url, - processingStage: .ingestion) - let repoId = UUID() - try await Repository(id: repoId, - package: pkg, - defaultBranch: "main", - name: "1", - owner: "foo").save(on: app.db) - - Current.git.commitCount = { @Sendable _ in 12 } - Current.git.firstCommitDate = { @Sendable _ in .t0 } - Current.git.lastCommitDate = { @Sendable _ in .t1 } - Current.git.getTags = { @Sendable _ in [.tag(1, 2, 3)] } - Current.git.hasBranch = { @Sendable _, _ in true } - Current.git.revisionInfo = { @Sendable _, _ in .init(commit: "sha", date: .t0) } - Current.git.shortlog = { @Sendable _ in + try await withDependencies { + $0.date.now = .t0 + } operation: { + // setup + // - package dump does not include toolsVersion, targets to simulate an "old version" + // - run analysis to create existing version + // - validate that initial state is reflected + // - then change input data in fields that are affecting existing versions (which `analysis` is "blind" to) + // - run analysis again to confirm "blindness" + // - run re-analysis and confirm changes are now reflected + let pkg = try await savePackage(on: app.db, + "https://github.com/foo/1".url, + processingStage: .ingestion) + let repoId = UUID() + try await Repository(id: repoId, + package: pkg, + defaultBranch: "main", + name: "1", + owner: "foo").save(on: app.db) + + Current.git.commitCount = { @Sendable _ in 12 } + Current.git.firstCommitDate = { @Sendable _ in .t0 } + Current.git.lastCommitDate = { @Sendable _ in .t1 } + Current.git.getTags = { @Sendable _ in [.tag(1, 2, 3)] } + Current.git.hasBranch = { @Sendable _, _ in true } + Current.git.revisionInfo = { @Sendable _, _ in .init(commit: "sha", date: .t0) } + Current.git.shortlog = { @Sendable _ in """ 10\tPerson 1 2\tPerson 2 """ - } - - Current.shell.run = { @Sendable cmd, path in - if cmd.description.hasSuffix("swift package dump-package") { - return #""" + } + + Current.shell.run = { @Sendable cmd, path in + if cmd.description.hasSuffix("swift package dump-package") { + return #""" { "name": "SPI-Server", "products": [], "targets": [] } """# + } + return "" } - return "" - } - do { - // run initial analysis and assert initial state for versions - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) - let versions = try await Version.query(on: app.db) - .with(\.$targets) - .all() - XCTAssertEqual(versions.map(\.toolsVersion), [nil, nil]) - XCTAssertEqual(versions.map { $0.targets.map(\.name) } , [[], []]) - XCTAssertEqual(versions.map(\.releaseNotes) , [nil, nil]) - } - do { - // Update state that would normally not be affecting existing versions, effectively simulating the situation where we only started parsing it after versions had already been created - Current.shell.run = { @Sendable cmd, path in - if cmd.description.hasSuffix("swift package dump-package") { - return #""" + do { + // run initial analysis and assert initial state for versions + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) + let versions = try await Version.query(on: app.db) + .with(\.$targets) + .all() + XCTAssertEqual(versions.map(\.toolsVersion), [nil, nil]) + XCTAssertEqual(versions.map { $0.targets.map(\.name) } , [[], []]) + XCTAssertEqual(versions.map(\.releaseNotes) , [nil, nil]) + } + do { + // Update state that would normally not be affecting existing versions, effectively simulating the situation where we only started parsing it after versions had already been created + Current.shell.run = { @Sendable cmd, path in + if cmd.description.hasSuffix("swift package dump-package") { + return #""" { "name": "SPI-Server", "products": [], @@ -92,52 +97,53 @@ class ReAnalyzeVersionsTests: AppTestCase { } } """# + } + return "" } - return "" - } - // also, update release notes to ensure mergeReleaseInfo is being called - let r = try await Repository.find(repoId, on: app.db).unwrap() - r.releases = [ - .mock(description: "rel 1.2.3", tagName: "1.2.3") - ] - try await r.save(on: app.db) - // Package has gained a SPI manifest - Current.loadSPIManifest = { path in - if path.hasSuffix("foo-1") { - return .init(builder: .init(configs: [.init(documentationTargets: ["DocTarget"])])) - } else { - return nil + // also, update release notes to ensure mergeReleaseInfo is being called + let r = try await Repository.find(repoId, on: app.db).unwrap() + r.releases = [ + .mock(description: "rel 1.2.3", tagName: "1.2.3") + ] + try await r.save(on: app.db) + // Package has gained a SPI manifest + Current.loadSPIManifest = { path in + if path.hasSuffix("foo-1") { + return .init(builder: .init(configs: [.init(documentationTargets: ["DocTarget"])])) + } else { + return nil + } } } - } - do { // assert running analysis again does not update existing versions - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) + do { // assert running analysis again does not update existing versions + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) + let versions = try await Version.query(on: app.db) + .with(\.$targets) + .all() + XCTAssertEqual(versions.map(\.toolsVersion), [nil, nil]) + XCTAssertEqual(versions.map { $0.targets.map(\.name) } , [[], []]) + XCTAssertEqual(versions.map(\.releaseNotes) , [nil, nil]) + XCTAssertEqual(versions.map(\.docArchives), [nil, nil]) + } + + // MUT + try await ReAnalyzeVersions.reAnalyzeVersions(client: app.client, + database: app.db, + before: Date.now, + refreshCheckouts: false, + limit: 10) + + // validate that re-analysis has now updated existing versions let versions = try await Version.query(on: app.db) .with(\.$targets) + .sort(\.$createdAt) .all() - XCTAssertEqual(versions.map(\.toolsVersion), [nil, nil]) - XCTAssertEqual(versions.map { $0.targets.map(\.name) } , [[], []]) - XCTAssertEqual(versions.map(\.releaseNotes) , [nil, nil]) - XCTAssertEqual(versions.map(\.docArchives), [nil, nil]) + XCTAssertEqual(versions.map(\.toolsVersion), ["5.3", "5.3"]) + XCTAssertEqual(versions.map { $0.targets.map(\.name) } , [["t1"], ["t1"]]) + XCTAssertEqual(versions.compactMap(\.releaseNotes) , ["rel 1.2.3"]) } - - // MUT - try await ReAnalyzeVersions.reAnalyzeVersions(client: app.client, - database: app.db, - before: Current.date(), - refreshCheckouts: false, - limit: 10) - - // validate that re-analysis has now updated existing versions - let versions = try await Version.query(on: app.db) - .with(\.$targets) - .sort(\.$createdAt) - .all() - XCTAssertEqual(versions.map(\.toolsVersion), ["5.3", "5.3"]) - XCTAssertEqual(versions.map { $0.targets.map(\.name) } , [["t1"], ["t1"]]) - XCTAssertEqual(versions.compactMap(\.releaseNotes) , ["rel 1.2.3"]) } func test_Package_fetchReAnalysisCandidates() async throws { @@ -178,66 +184,69 @@ class ReAnalyzeVersionsTests: AppTestCase { // This is to ensure our candidate selection shrinks and we don't // churn over and over on failing versions. let cutoff = Date.t1 - Current.date = { .t2 } - let pkg = try await savePackage(on: app.db, - "https://github.com/foo/1".url, - processingStage: .ingestion) - try await Repository(package: pkg, - defaultBranch: "main").save(on: app.db) - Current.git.commitCount = { @Sendable _ in 12 } - Current.git.firstCommitDate = { @Sendable _ in .t0 } - Current.git.lastCommitDate = { @Sendable _ in .t1 } - Current.git.getTags = { @Sendable _ in [] } - Current.git.hasBranch = { @Sendable _, _ in true } - Current.git.revisionInfo = { @Sendable _, _ in .init(commit: "sha", date: .t0) } - Current.git.shortlog = { @Sendable _ in + try await withDependencies { + $0.date.now = .t2 + } operation: { + let pkg = try await savePackage(on: app.db, + "https://github.com/foo/1".url, + processingStage: .ingestion) + try await Repository(package: pkg, + defaultBranch: "main").save(on: app.db) + Current.git.commitCount = { @Sendable _ in 12 } + Current.git.firstCommitDate = { @Sendable _ in .t0 } + Current.git.lastCommitDate = { @Sendable _ in .t1 } + Current.git.getTags = { @Sendable _ in [] } + Current.git.hasBranch = { @Sendable _, _ in true } + Current.git.revisionInfo = { @Sendable _, _ in .init(commit: "sha", date: .t0) } + Current.git.shortlog = { @Sendable _ in """ 10\tPerson 1 2\tPerson 2 """ - } - Current.shell.run = { @Sendable cmd, path in - if cmd == .swiftDumpPackage { - return #""" + } + Current.shell.run = { @Sendable cmd, path in + if cmd == .swiftDumpPackage { + return #""" { "name": "foo-1", "products": [], "targets": [{"name": "t1", "type": "executable"}] } """# + } + return "" + } + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) + try await setAllVersionsUpdatedAt(app.db, updatedAt: .t0) + do { + let candidates = try await Package + .fetchReAnalysisCandidates(app.db, before: cutoff, limit: 10) + XCTAssertEqual(candidates.count, 1) } - return "" - } - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) - try await setAllVersionsUpdatedAt(app.db, updatedAt: .t0) - do { - let candidates = try await Package - .fetchReAnalysisCandidates(app.db, before: cutoff, limit: 10) - XCTAssertEqual(candidates.count, 1) - } - Current.shell.run = { @Sendable cmd, path in - if cmd == .swiftDumpPackage { - // simulate error during package dump - struct Error: Swift.Error { } - throw Error() + Current.shell.run = { @Sendable cmd, path in + if cmd == .swiftDumpPackage { + // simulate error during package dump + struct Error: Swift.Error { } + throw Error() + } + return "" } - return "" - } - // MUT - try await ReAnalyzeVersions.reAnalyzeVersions(client: app.client, - database: app.db, - before: Current.date(), - refreshCheckouts: false, - limit: 10) + // MUT + try await ReAnalyzeVersions.reAnalyzeVersions(client: app.client, + database: app.db, + before: Date.now, + refreshCheckouts: false, + limit: 10) - // validate - let candidates = try await Package - .fetchReAnalysisCandidates(app.db, before: cutoff, limit: 10) - XCTAssertEqual(candidates.count, 0) + // validate + let candidates = try await Package + .fetchReAnalysisCandidates(app.db, before: cutoff, limit: 10) + XCTAssertEqual(candidates.count, 0) + } } } diff --git a/Tests/AppTests/ScoreTests.swift b/Tests/AppTests/ScoreTests.swift index 54861a827..85ba66063 100644 --- a/Tests/AppTests/ScoreTests.swift +++ b/Tests/AppTests/ScoreTests.swift @@ -12,286 +12,300 @@ // See the License for the specific language governing permissions and // limitations under the License. +import XCTest + @testable import App -import XCTest +import Dependencies class ScoreTests: AppTestCase { func test_computeBreakdown() throws { - XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .none, - releaseCount: 0, - likeCount: 0, - isArchived: false, - numberOfDependencies: nil, - lastActivityAt: Current.date().adding(days: -400), - hasDocumentation: false, - hasReadme: false, - numberOfContributors: 0, - hasTestTargets: false)).score, - 20) - XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .incompatibleWithAppStore, - releaseCount: 0, - likeCount: 0, - isArchived: false, - numberOfDependencies: nil, - lastActivityAt: Current.date().adding(days: -400), - hasDocumentation: false, - hasReadme: false, - numberOfContributors: 0, - hasTestTargets: false)).score, - 23) - XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, - releaseCount: 0, - likeCount: 0, - isArchived: false, - numberOfDependencies: nil, - lastActivityAt: Current.date().adding(days: -400), - hasDocumentation: false, - hasReadme: false, - numberOfContributors: 0, - hasTestTargets: false)).score, - 30) - XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, - releaseCount: 10, - likeCount: 0, - isArchived: false, - numberOfDependencies: nil, - lastActivityAt: Current.date().adding(days: -400), - hasDocumentation: false, - hasReadme: false, - numberOfContributors: 0, - hasTestTargets: false)).score, - 40) - XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, - releaseCount: 10, - likeCount: 50, - isArchived: false, - numberOfDependencies: nil, - lastActivityAt: Current.date().adding(days: -400), - hasDocumentation: false, - hasReadme: false, - numberOfContributors: 0, - hasTestTargets: false)).score, - 50) - XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, - releaseCount: 10, - likeCount: 50, - isArchived: true, - numberOfDependencies: nil, - lastActivityAt: Current.date().adding(days: -400), - hasDocumentation: false, - hasReadme: false, - numberOfContributors: 0, - hasTestTargets: false)).score, - 30) - XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, - releaseCount: 20, - likeCount: 20_000, - isArchived: false, - numberOfDependencies: nil, - lastActivityAt: Current.date().adding(days: -400), - hasDocumentation: false, - hasReadme: false, - numberOfContributors: 0, - hasTestTargets: false)).score, - 87) - XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, - releaseCount: 20, - likeCount: 20_000, - isArchived: false, - numberOfDependencies: 4, - lastActivityAt: Current.date().adding(days: -400), - hasDocumentation: false, - hasReadme: false, - numberOfContributors: 0, - hasTestTargets: false)).score, - 89) - XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, - releaseCount: 20, - likeCount: 20_000, - isArchived: false, - numberOfDependencies: 2, - lastActivityAt: Current.date().adding(days: -400), - hasDocumentation: false, - hasReadme: false, - numberOfContributors: 0, - hasTestTargets: false)).score, - 92) - XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, - releaseCount: 20, - likeCount: 20_000, - isArchived: false, - numberOfDependencies: 2, - lastActivityAt: Current.date().adding(days: -400), - hasDocumentation: false, - hasReadme: false, - numberOfContributors: 0, - hasTestTargets: false)).score, - 92) - XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, - releaseCount: 20, - likeCount: 20_000, - isArchived: false, - numberOfDependencies: 2, - lastActivityAt: Current.date().adding(days: -300), - hasDocumentation: false, - hasReadme: false, - numberOfContributors: 0, - hasTestTargets: false)).score, - 97) - XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, - releaseCount: 20, - likeCount: 20_000, - isArchived: false, - numberOfDependencies: 2, - lastActivityAt: Current.date().adding(days: -100), - hasDocumentation: false, - hasReadme: false, - numberOfContributors: 0, - hasTestTargets: false)).score, - 102) - XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, - releaseCount: 20, - likeCount: 20_000, - isArchived: false, - numberOfDependencies: 2, - lastActivityAt: Current.date().adding(days: -10), - hasDocumentation: false, - hasReadme: false, - numberOfContributors: 0, - hasTestTargets: false)).score, - 107) - XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, - releaseCount: 20, - likeCount: 20_000, - isArchived: false, - numberOfDependencies: 2, - lastActivityAt: Current.date().adding(days: -10), - hasDocumentation: true, - hasReadme: false, - numberOfContributors: 0, - hasTestTargets: false)).score, - 122) - XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, - releaseCount: 20, - likeCount: 20_000, - isArchived: false, - numberOfDependencies: 2, - lastActivityAt: Current.date().adding(days: -10), - hasDocumentation: true, - hasReadme: false, - numberOfContributors: 5, - hasTestTargets: false)).score, - 127) - XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, - releaseCount: 20, - likeCount: 20_000, - isArchived: false, - numberOfDependencies: 2, - lastActivityAt: Current.date().adding(days: -10), - hasDocumentation: true, - hasReadme: false, - numberOfContributors: 20, - hasTestTargets: false)).score, - 132) - XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, - releaseCount: 20, - likeCount: 20_000, - isArchived: false, - numberOfDependencies: 2, - lastActivityAt: Current.date().adding(days: -10), - hasDocumentation: false, - hasReadme: false, - numberOfContributors: 0, - hasTestTargets: false)).score, - 107) - XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, - releaseCount: 20, - likeCount: 20_000, - isArchived: false, - numberOfDependencies: 2, - lastActivityAt: Current.date().adding(days: -10), - hasDocumentation: false, - hasReadme: false, - numberOfContributors: 0, - hasTestTargets: true)).score, - 112) - XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, - releaseCount: 20, - likeCount: 20_000, - isArchived: false, - numberOfDependencies: 2, - lastActivityAt: Current.date().adding(days: -10), - hasDocumentation: false, - hasReadme: true, - numberOfContributors: 0, - hasTestTargets: false)).score, - 122) + withDependencies { + $0.date.now = .t0 + } operation: { + XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .none, + releaseCount: 0, + likeCount: 0, + isArchived: false, + numberOfDependencies: nil, + lastActivityAt: .t0.adding(days: -400), + hasDocumentation: false, + hasReadme: false, + numberOfContributors: 0, + hasTestTargets: false)).score, + 20) + XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .incompatibleWithAppStore, + releaseCount: 0, + likeCount: 0, + isArchived: false, + numberOfDependencies: nil, + lastActivityAt: .t0.adding(days: -400), + hasDocumentation: false, + hasReadme: false, + numberOfContributors: 0, + hasTestTargets: false)).score, + 23) + XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, + releaseCount: 0, + likeCount: 0, + isArchived: false, + numberOfDependencies: nil, + lastActivityAt: .t0.adding(days: -400), + hasDocumentation: false, + hasReadme: false, + numberOfContributors: 0, + hasTestTargets: false)).score, + 30) + XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, + releaseCount: 10, + likeCount: 0, + isArchived: false, + numberOfDependencies: nil, + lastActivityAt: .t0.adding(days: -400), + hasDocumentation: false, + hasReadme: false, + numberOfContributors: 0, + hasTestTargets: false)).score, + 40) + XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, + releaseCount: 10, + likeCount: 50, + isArchived: false, + numberOfDependencies: nil, + lastActivityAt: .t0.adding(days: -400), + hasDocumentation: false, + hasReadme: false, + numberOfContributors: 0, + hasTestTargets: false)).score, + 50) + XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, + releaseCount: 10, + likeCount: 50, + isArchived: true, + numberOfDependencies: nil, + lastActivityAt: .t0.adding(days: -400), + hasDocumentation: false, + hasReadme: false, + numberOfContributors: 0, + hasTestTargets: false)).score, + 30) + XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, + releaseCount: 20, + likeCount: 20_000, + isArchived: false, + numberOfDependencies: nil, + lastActivityAt: .t0.adding(days: -400), + hasDocumentation: false, + hasReadme: false, + numberOfContributors: 0, + hasTestTargets: false)).score, + 87) + XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, + releaseCount: 20, + likeCount: 20_000, + isArchived: false, + numberOfDependencies: 4, + lastActivityAt: .t0.adding(days: -400), + hasDocumentation: false, + hasReadme: false, + numberOfContributors: 0, + hasTestTargets: false)).score, + 89) + XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, + releaseCount: 20, + likeCount: 20_000, + isArchived: false, + numberOfDependencies: 2, + lastActivityAt: .t0.adding(days: -400), + hasDocumentation: false, + hasReadme: false, + numberOfContributors: 0, + hasTestTargets: false)).score, + 92) + XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, + releaseCount: 20, + likeCount: 20_000, + isArchived: false, + numberOfDependencies: 2, + lastActivityAt: .t0.adding(days: -400), + hasDocumentation: false, + hasReadme: false, + numberOfContributors: 0, + hasTestTargets: false)).score, + 92) + XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, + releaseCount: 20, + likeCount: 20_000, + isArchived: false, + numberOfDependencies: 2, + lastActivityAt: .t0.adding(days: -300), + hasDocumentation: false, + hasReadme: false, + numberOfContributors: 0, + hasTestTargets: false)).score, + 97) + XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, + releaseCount: 20, + likeCount: 20_000, + isArchived: false, + numberOfDependencies: 2, + lastActivityAt: .t0.adding(days: -100), + hasDocumentation: false, + hasReadme: false, + numberOfContributors: 0, + hasTestTargets: false)).score, + 102) + XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, + releaseCount: 20, + likeCount: 20_000, + isArchived: false, + numberOfDependencies: 2, + lastActivityAt: .t0.adding(days: -10), + hasDocumentation: false, + hasReadme: false, + numberOfContributors: 0, + hasTestTargets: false)).score, + 107) + XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, + releaseCount: 20, + likeCount: 20_000, + isArchived: false, + numberOfDependencies: 2, + lastActivityAt: .t0.adding(days: -10), + hasDocumentation: true, + hasReadme: false, + numberOfContributors: 0, + hasTestTargets: false)).score, + 122) + XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, + releaseCount: 20, + likeCount: 20_000, + isArchived: false, + numberOfDependencies: 2, + lastActivityAt: .t0.adding(days: -10), + hasDocumentation: true, + hasReadme: false, + numberOfContributors: 5, + hasTestTargets: false)).score, + 127) + XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, + releaseCount: 20, + likeCount: 20_000, + isArchived: false, + numberOfDependencies: 2, + lastActivityAt: .t0.adding(days: -10), + hasDocumentation: true, + hasReadme: false, + numberOfContributors: 20, + hasTestTargets: false)).score, + 132) + XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, + releaseCount: 20, + likeCount: 20_000, + isArchived: false, + numberOfDependencies: 2, + lastActivityAt: .t0.adding(days: -10), + hasDocumentation: false, + hasReadme: false, + numberOfContributors: 0, + hasTestTargets: false)).score, + 107) + XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, + releaseCount: 20, + likeCount: 20_000, + isArchived: false, + numberOfDependencies: 2, + lastActivityAt: .t0.adding(days: -10), + hasDocumentation: false, + hasReadme: false, + numberOfContributors: 0, + hasTestTargets: true)).score, + 112) + XCTAssertEqual(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore, + releaseCount: 20, + likeCount: 20_000, + isArchived: false, + numberOfDependencies: 2, + lastActivityAt: .t0.adding(days: -10), + hasDocumentation: false, + hasReadme: true, + numberOfContributors: 0, + hasTestTargets: false)).score, + 122) + } } func test_computeDetails() async throws { - // setup - let pkg = try await savePackage(on: app.db, "1") - try await Repository(package: pkg, defaultBranch: "default", stars: 10_000).save(on: app.db) - try await Version(package: pkg, - docArchives: [.init(name: "archive1", title: "Archive One")], - reference: .branch("default"), - resolvedDependencies: [], - swiftVersions: ["5"].asSwiftVersions).save(on: app.db) - for idx in (0..<20) { - try await Version(package: pkg, reference: .tag(.init(idx, 0, 0))).save(on: app.db) - } - let jpr = try await Package.fetchCandidate(app.db, id: pkg.id!) - // update versions - let versions = try await Analyze.updateLatestVersions(on: app.db, package: jpr) + try await withDependencies { + $0.date.now = .now + } operation: { + // setup + let pkg = try await savePackage(on: app.db, "1") + try await Repository(package: pkg, defaultBranch: "default", stars: 10_000).save(on: app.db) + try await Version(package: pkg, + docArchives: [.init(name: "archive1", title: "Archive One")], + reference: .branch("default"), + resolvedDependencies: [], + swiftVersions: ["5"].asSwiftVersions).save(on: app.db) + for idx in (0..<20) { + try await Version(package: pkg, reference: .tag(.init(idx, 0, 0))).save(on: app.db) + } + let jpr = try await Package.fetchCandidate(app.db, id: pkg.id!) + // update versions + let versions = try await Analyze.updateLatestVersions(on: app.db, package: jpr) - // MUT - let details = Score.computeDetails(repo: jpr.repository, versions: versions) + // MUT + let details = Score.computeDetails(repo: jpr.repository, versions: versions) - do { // validate - let details = try XCTUnwrap(details) - XCTAssertEqual(details.scoreBreakdown, [ - .archive: 20, - .dependencies: 5, - .documentation: 15, - .releases: 20, - .stars: 37, - ]) - XCTAssertEqual(details.score, 97) + do { // validate + let details = try XCTUnwrap(details) + XCTAssertEqual(details.scoreBreakdown, [ + .archive: 20, + .dependencies: 5, + .documentation: 15, + .releases: 20, + .stars: 37, + ]) + XCTAssertEqual(details.score, 97) + } } } func test_computeDetails_unknown_resolvedDependencies() async throws { - // setup - let pkg = try await savePackage(on: app.db, "1") - try await Repository(package: pkg, defaultBranch: "default", stars: 10_000).save(on: app.db) - try await Version(package: pkg, - docArchives: [.init(name: "archive1", title: "Archive One")], - reference: .branch("default"), - resolvedDependencies: nil, - swiftVersions: ["5"].asSwiftVersions).save(on: app.db) - for idx in (0..<20) { - try await Version(package: pkg, reference: .tag(.init(idx, 0, 0))).save(on: app.db) - } - let jpr = try await Package.fetchCandidate(app.db, id: pkg.id!) - // update versions - let versions = try await Analyze.updateLatestVersions(on: app.db, package: jpr) + try await withDependencies { + $0.date.now = .now + } operation: { + // setup + let pkg = try await savePackage(on: app.db, "1") + try await Repository(package: pkg, defaultBranch: "default", stars: 10_000).save(on: app.db) + try await Version(package: pkg, + docArchives: [.init(name: "archive1", title: "Archive One")], + reference: .branch("default"), + resolvedDependencies: nil, + swiftVersions: ["5"].asSwiftVersions).save(on: app.db) + for idx in (0..<20) { + try await Version(package: pkg, reference: .tag(.init(idx, 0, 0))).save(on: app.db) + } + let jpr = try await Package.fetchCandidate(app.db, id: pkg.id!) + // update versions + let versions = try await Analyze.updateLatestVersions(on: app.db, package: jpr) - // MUT - let details = Score.computeDetails(repo: jpr.repository, versions: versions) + // MUT + let details = Score.computeDetails(repo: jpr.repository, versions: versions) - do { // validate - let details = try XCTUnwrap(details) - XCTAssertEqual(details.scoreBreakdown, [ - .archive: 20, - // no .dependencies category - .documentation: 15, - .releases: 20, - .stars: 37, - ]) - XCTAssertEqual(details.score, 92) + do { // validate + let details = try XCTUnwrap(details) + XCTAssertEqual(details.scoreBreakdown, [ + .archive: 20, + // no .dependencies category + .documentation: 15, + .releases: 20, + .stars: 37, + ]) + XCTAssertEqual(details.score, 92) + } } } diff --git a/Tests/AppTests/SitemapTests.swift b/Tests/AppTests/SitemapTests.swift index 6fb7c5343..f09c79a07 100644 --- a/Tests/AppTests/SitemapTests.swift +++ b/Tests/AppTests/SitemapTests.swift @@ -27,7 +27,7 @@ class SitemapTests: SnapshotTestCase { let packages = (0..<3).map { Package(url: "\($0)".url) } try await packages.save(on: app.db) try await packages.map { try Repository(package: $0, defaultBranch: "default", - lastCommitDate: Current.date(), name: $0.url, + lastCommitDate: .t0, name: $0.url, owner: "foo") }.save(on: app.db) try await packages.map { try Version(package: $0, packageName: "foo", reference: .branch("default")) }.save(on: app.db) @@ -117,7 +117,7 @@ class SitemapTests: SnapshotTestCase { let package = Package(url: URL(stringLiteral: "https://example.com/owner/repo0")) try await package.save(on: app.db) try await Repository(package: package, defaultBranch: "default", - lastCommitDate: Current.date(), + lastCommitDate: Date.now, name: "Repo0", owner: "Owner").save(on: app.db) try await Version(package: package, commit: "123456", @@ -158,7 +158,7 @@ class SitemapTests: SnapshotTestCase { let package = Package(url: URL(stringLiteral: "https://example.com/owner/repo0")) try await package.save(on: app.db) try await Repository(package: package, defaultBranch: "a/b", - lastCommitDate: Current.date(), + lastCommitDate: Date.now, name: "Repo0", owner: "Owner").save(on: app.db) try await Version(package: package, commit: "123456", @@ -198,7 +198,7 @@ class SitemapTests: SnapshotTestCase { let package = Package(url: URL(stringLiteral: "https://example.com/owner/repo0")) try await package.save(on: app.db) try await Repository(package: package, defaultBranch: "default", - lastCommitDate: Current.date(), + lastCommitDate: .t0, name: "Repo0", owner: "Owner").save(on: app.db) try await Version(package: package, latest: .defaultBranch, packageName: "SomePackage", reference: .branch("default")).save(on: app.db) @@ -222,7 +222,7 @@ class SitemapTests: SnapshotTestCase { let package = Package(url: URL(stringLiteral: "https://example.com/owner/repo0")) try await package.save(on: app.db) try await Repository(package: package, defaultBranch: "default", - lastCommitDate: Current.date(), + lastCommitDate: .t0, name: "Repo0", owner: "Owner").save(on: app.db) try await Version(package: package, latest: .defaultBranch, packageName: "SomePackage", reference: .branch("default")).save(on: app.db) diff --git a/Tests/AppTests/SnapshotTestCase.swift b/Tests/AppTests/SnapshotTestCase.swift index 962be1b3c..2913e9112 100644 --- a/Tests/AppTests/SnapshotTestCase.swift +++ b/Tests/AppTests/SnapshotTestCase.swift @@ -16,20 +16,23 @@ import Foundation import SnapshotTesting +import Dependencies class SnapshotTestCase: AppTestCase { override func setUpWithError() throws { try super.setUpWithError() - - Current.date = { Date(timeIntervalSince1970: 0) } } override func invokeTest() { // To force a re-record of all snapshots, use `record: .all` rather than `record: .missing`. withSnapshotTesting(record: .missing, diffTool: .ksdiff) { - super.invokeTest() + withDependencies { + $0.date.now = .t0 + } operation: { + super.invokeTest() + } } } diff --git a/Tests/AppTests/WebpageSnapshotTests.swift b/Tests/AppTests/WebpageSnapshotTests.swift index 846e6e9b3..55292228f 100644 --- a/Tests/AppTests/WebpageSnapshotTests.swift +++ b/Tests/AppTests/WebpageSnapshotTests.swift @@ -357,7 +357,7 @@ class WebpageSnapshotTests: SnapshotTestCase { repositoryOwner: "package", stars: 1111, // 24 hours + 4 hours to take it firmly into "one day ago" for the snapshot. - lastActivityAt: Current.date().adding(hours: -28), + lastActivityAt: .t0.adding(hours: -28), summary: "This is a package filled with ones.", keywords: ["one", "1"], hasDocs: false @@ -372,7 +372,7 @@ class WebpageSnapshotTests: SnapshotTestCase { repositoryOwner: "package", stars: 2222, // 48 hours + 4 hours to take it firmly into "two days ago" for the snapshot. - lastActivityAt: Current.date().adding(hours: -52), + lastActivityAt: .t0.adding(hours: -52), summary: "This is a package filled with twos.", keywords: ["two", "2"], hasDocs: false @@ -387,7 +387,7 @@ class WebpageSnapshotTests: SnapshotTestCase { repositoryOwner: "package", stars: 3333, // 72 hours + 4 hours to take it firmly into "two days ago" for the snapshot. - lastActivityAt: Current.date().adding(hours: -76), + lastActivityAt: .t0.adding(hours: -76), summary: "This is a package filled with threes.", keywords: ["three", "3"], hasDocs: false @@ -402,7 +402,7 @@ class WebpageSnapshotTests: SnapshotTestCase { repositoryOwner: "package", stars: 4444, // 72 hours + 4 hours to take it firmly into "two days ago" for the snapshot. - lastActivityAt: Current.date().adding(hours: -76), + lastActivityAt: .t0.adding(hours: -76), summary: "This is a package filled with fours.", keywords: ["four", "4"], hasDocs: false