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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Sources/App/Commands/Analyze.swift
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,8 @@ extension Analyze {

do {
let packageManifest = try await dumpPackage(at: cacheDir)
let spiManifest = Current.loadSPIManifest(cacheDir)
@Dependency(\.environment) var environment
let spiManifest = environment.loadSPIManifest(cacheDir)

return PackageInfo(packageManifest: packageManifest,
spiManifest: spiManifest)
Expand Down
13 changes: 0 additions & 13 deletions Sources/App/Core/AppEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ struct AppEnvironment: Sendable {
var gitlabApiToken: @Sendable () -> String?
var gitlabPipelineToken: @Sendable () -> String?
var gitlabPipelineLimit: @Sendable () -> Int
var hideStagingBanner: @Sendable () -> Bool
var maintenanceMessage: @Sendable () -> String?
var loadSPIManifest: @Sendable (String) -> SPIManifest.Manifest?
var logger: @Sendable () -> Logger
var metricsPushGatewayUrl: @Sendable () -> String?
var plausibleBackendReportingSiteID: @Sendable () -> String?
Expand All @@ -46,7 +43,6 @@ struct AppEnvironment: Sendable {
_ readme: String) async throws(S3Readme.Error) -> String
var storeS3ReadmeImages: @Sendable (_ client: Client,
_ imagesToCache: [Github.Readme.ImageToCache]) async throws(S3Readme.Error) -> Void
var timeZone: @Sendable () -> TimeZone
var triggerBuild: @Sendable (_ client: Client,
_ buildId: Build.Id,
_ cloneURL: String,
Expand Down Expand Up @@ -78,14 +74,6 @@ extension AppEnvironment {
Environment.get("GITLAB_PIPELINE_LIMIT").flatMap(Int.init)
?? Constants.defaultGitlabPipelineLimit
},
hideStagingBanner: {
Environment.get("HIDE_STAGING_BANNER").flatMap(\.asBool)
?? Constants.defaultHideStagingBanner
},
maintenanceMessage: {
Environment.get("MAINTENANCE_MESSAGE").flatMap(\.trimmed)
},
loadSPIManifest: { path in SPIManifest.Manifest.load(in: path) },
logger: { logger },
metricsPushGatewayUrl: { Environment.get("METRICS_PUSHGATEWAY_URL") },
plausibleBackendReportingSiteID: { Environment.get("PLAUSIBLE_BACKEND_REPORTING_SITE_ID") },
Expand All @@ -107,7 +95,6 @@ extension AppEnvironment {
storeS3ReadmeImages: { client, images throws(S3Readme.Error) in
try await S3Readme.storeReadmeImages(client: client, imagesToCache: images)
},
timeZone: { .current },
triggerBuild: { client, buildId, cloneURL, isDocBuild, platform, ref, swiftVersion, versionID in
try await Gitlab.Builder.triggerBuild(client: client,
buildId: buildId,
Expand Down
15 changes: 14 additions & 1 deletion Sources/App/Core/Dependencies/EnvironmentClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import Dependencies
import DependenciesMacros
import SPIManifest
import Vapor


Expand All @@ -40,6 +41,9 @@ struct EnvironmentClient {
var collectionSigningPrivateKey: @Sendable () -> Data?
var current: @Sendable () -> Environment = { XCTFail("current"); return .development }
var dbId: @Sendable () -> String?
var hideStagingBanner: @Sendable () -> Bool = { XCTFail("hideStagingBanner"); return Constants.defaultHideStagingBanner }
var loadSPIManifest: @Sendable (String) -> SPIManifest.Manifest?
var maintenanceMessage: @Sendable () -> String?
var mastodonCredentials: @Sendable () -> Mastodon.Credentials?
var random: @Sendable (_ range: ClosedRange<Double>) -> Double = { XCTFail("random"); return Double.random(in: $0) }

Expand Down Expand Up @@ -102,6 +106,14 @@ extension EnvironmentClient: DependencyKey {
},
current: { (try? Environment.detect()) ?? .development },
dbId: { Environment.get("DATABASE_ID") },
hideStagingBanner: {
Environment.get("HIDE_STAGING_BANNER").flatMap(\.asBool)
?? Constants.defaultHideStagingBanner
},
loadSPIManifest: { path in SPIManifest.Manifest.load(in: path) },
maintenanceMessage: {
Environment.get("MAINTENANCE_MESSAGE").flatMap(\.trimmed)
},
mastodonCredentials: {
Environment.get("MASTODON_ACCESS_TOKEN")
.map(Mastodon.Credentials.init(accessToken:))
Expand Down Expand Up @@ -134,7 +146,7 @@ extension EnvironmentClient {
extension EnvironmentClient: TestDependencyKey {
static var testValue: Self {
// sas 2024-11-22:
// For a few attributes we provide a default value overriding the XCTFail, because theis use is too
// For a few attributes we provide a default value overriding the XCTFail, because their use is too
// pervasive and would require the vast majority of tests to be wrapped with `withDependencies`.
// We can do so at a later time once more tests are transitioned over for other dependencies. This is
// the exact same default behaviour we had with the Current dependency injection. It did not have
Expand All @@ -143,6 +155,7 @@ extension EnvironmentClient: TestDependencyKey {
var mock = Self()
mock.appVersion = { "test" }
mock.current = { .development }
mock.hideStagingBanner = { false }
return mock
}
}
Expand Down
9 changes: 9 additions & 0 deletions Sources/App/Core/Dependencies/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,14 @@ extension HTTPClient.Response {
static var notFound: Self { .init(status: .notFound) }
static var tooManyRequests: Self { .init(status: .tooManyRequests) }
static var ok: Self { .init(status: .ok) }

static func ok(body: String, headers: HTTPHeaders = .init()) -> Self {
.init(status: .ok, headers: headers, body: .init(string: body))
}

static func ok<T: Encodable>(jsonEncode value: T, headers: HTTPHeaders = .init()) throws -> Self {
let data = try JSONEncoder().encode(value)
return .init(status: .ok, headers: headers, body: .init(data: data))
}
}
#endif
9 changes: 6 additions & 3 deletions Sources/App/Core/Extensions/Date+ext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,29 @@ import Dependencies

extension DateFormatter {
static var mediumDateFormatter: DateFormatter {
@Dependency(\.timeZone) var timeZone
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.locale = .init(identifier: "en_GB")
formatter.timeZone = Current.timeZone()
formatter.timeZone = timeZone
return formatter
}

static var longDateFormatter: DateFormatter {
@Dependency(\.timeZone) var timeZone
let formatter = DateFormatter()
formatter.dateStyle = .long
formatter.locale = .init(identifier: "en_GB")
formatter.timeZone = Current.timeZone()
formatter.timeZone = timeZone
return formatter
}

static var yearMonthDayDateFormatter: DateFormatter {
@Dependency(\.timeZone) var timeZone
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
formatter.locale = .init(identifier: "en_GB")
formatter.timeZone = Current.timeZone()
formatter.timeZone = timeZone
return formatter
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/App/Views/PublicPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,8 @@ class PublicPage {
/// A staging banner, which only appears on the staging/development server.
/// - Returns: Either a <div> element, or nothing.
final func stagingBanner() -> Node<HTML.BodyContext> {
guard !Current.hideStagingBanner() else { return .empty }
@Dependency(\.environment) var environment
guard !environment.hideStagingBanner() else { return .empty }
if environment.current() == .development {
return .div(
.class("staging"),
Expand Down
2 changes: 1 addition & 1 deletion Sources/App/routes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func routes(_ app: Application) throws {

do { // home page
app.get { req in
if let maintenanceMessage = Current.maintenanceMessage() {
if let maintenanceMessage = environment.maintenanceMessage() {
let model = MaintenanceMessageIndex.Model(markdown: maintenanceMessage)
return MaintenanceMessageIndex.View(path: req.url.path, model: model).document()
} else {
Expand Down
170 changes: 93 additions & 77 deletions Tests/AppTests/AnalyzeErrorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,102 +113,118 @@ final class AnalyzeErrorTests: AppTestCase {
}

func test_analyze_refreshCheckout_failed() async throws {
Current.shell.run = { @Sendable cmd, path in
switch cmd {
case _ where cmd.description.contains("git clone https://github.com/foo/1"):
throw SimulatedError()
try await withDependencies {
$0.environment.loadSPIManifest = { _ in nil }
} 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()
case .gitFetchAndPruneTags where path.hasSuffix("foo-1"):
throw SimulatedError()

default:
return try Self.defaultShellRun(cmd, path)
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.environment.loadSPIManifest = { _ in nil }
} 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.environment.loadSPIManifest = { _ in nil }
} 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.environment.loadSPIManifest = { _ in nil }
} 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)")
}
}

Expand Down
Loading
Loading