Skip to content
Merged
2 changes: 1 addition & 1 deletion Resources/ChartData/rfs6-errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,4 +254,4 @@
}
]
}
]
]
25 changes: 16 additions & 9 deletions Sources/App/Commands/Analyze.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,15 @@ extension Analyze {


static func trimCheckouts() throws {
@Dependency(\.fileManager) var fileManager
let checkoutDir = URL(
fileURLWithPath: Current.fileManager.checkoutsDirectory(),
fileURLWithPath: fileManager.checkoutsDirectory(),
isDirectory: true
)
try Current.fileManager.contentsOfDirectory(atPath: checkoutDir.path)
try fileManager.contentsOfDirectory(atPath: checkoutDir.path)
.map { dir -> (String, Date)? in
let url = checkoutDir.appendingPathComponent(dir)
guard let mod = try Current.fileManager
guard let mod = try fileManager
.attributesOfItem(atPath: url.path)[.modificationDate] as? Date
else { return nil }
return (url.path, mod)
Expand Down Expand Up @@ -139,7 +140,8 @@ extension Analyze {
AppMetrics.analyzeCandidatesCount?.set(packages.count)

// get or create directory
let checkoutDir = Current.fileManager.checkoutsDirectory()
@Dependency(\.fileManager) var fileManager
let checkoutDir = fileManager.checkoutsDirectory()
Current.logger().info("Checkout directory: \(checkoutDir)")
if !Current.fileManager.fileExists(atPath: checkoutDir) {
try await createCheckoutsDirectory(client: client, path: checkoutDir)
Expand Down Expand Up @@ -251,8 +253,9 @@ extension Analyze {
/// - Throws: Shell errors
static func clone(cacheDir: String, url: String) async throws {
Current.logger().info("cloning \(url) to \(cacheDir)")
@Dependency(\.fileManager) var fileManager
try await Current.shell.run(command: .gitClone(url: URL(string: url)!, to: cacheDir),
at: Current.fileManager.checkoutsDirectory())
at: fileManager.checkoutsDirectory())
}


Expand Down Expand Up @@ -286,7 +289,8 @@ extension Analyze {
/// - Parameters:
/// - package: `Package` to refresh
static func refreshCheckout(package: Joined<Package, Repository>) async throws {
guard let cacheDir = Current.fileManager.cacheDirectoryPath(for: package.model) else {
@Dependency(\.fileManager) var fileManager
guard let cacheDir = fileManager.cacheDirectoryPath(for: package.model) else {
throw AppError.invalidPackageCachePath(package.model.id, package.model.url)
}

Expand Down Expand Up @@ -322,7 +326,8 @@ extension Analyze {
guard let repo = package.repository else {
throw AppError.genericError(package.model.id, "updateRepository: no repository")
}
guard let gitDirectory = Current.fileManager.cacheDirectoryPath(for: package.model) else {
@Dependency(\.fileManager) var fileManager
guard let gitDirectory = fileManager.cacheDirectoryPath(for: package.model) else {
throw AppError.invalidPackageCachePath(package.model.id, package.model.url)
}

Expand Down Expand Up @@ -375,7 +380,8 @@ extension Analyze {
/// - Returns: future with incoming `Version`s
static func getIncomingVersions(client: Client,
package: Joined<Package, Repository>) async throws -> [Version] {
guard let cacheDir = Current.fileManager.cacheDirectoryPath(for: package.model) else {
@Dependency(\.fileManager) var fileManager
guard let cacheDir = fileManager.cacheDirectoryPath(for: package.model) else {
throw AppError.invalidPackageCachePath(package.model.id, package.model.url)
}

Expand Down Expand Up @@ -546,7 +552,8 @@ extension Analyze {
/// - Returns: `Result` with `Manifest` data
static func getPackageInfo(package: Joined<Package, Repository>, version: Version) async throws -> PackageInfo {
// check out version in cache directory
guard let cacheDir = Current.fileManager.cacheDirectoryPath(for: package.model) else {
@Dependency(\.fileManager) var fileManager
guard let cacheDir = fileManager.cacheDirectoryPath(for: package.model) else {
throw AppError.invalidPackageCachePath(package.model.id,
package.model.url)
}
Expand Down
3 changes: 2 additions & 1 deletion Sources/App/Commands/ReAnalyzeVersions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ enum ReAnalyzeVersions {
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 }
@Dependency(\.fileManager) var fileManager
guard let cacheDir = fileManager.cacheDirectoryPath(for: pkg.model) else { return }
if !Current.fileManager.fileExists(atPath: cacheDir) || refreshCheckouts {
try await Analyze.refreshCheckout(package: pkg)
}
Expand Down
23 changes: 0 additions & 23 deletions Sources/App/Core/AppEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,23 +45,12 @@ extension AppEnvironment {


struct FileManager: Sendable {
var attributesOfItem: @Sendable (_ path: String) throws -> [FileAttributeKey : Any]
var contentsOfDirectory: @Sendable (_ path: String) throws -> [String]
var contents: @Sendable (_ atPath: String) -> Data?
var checkoutsDirectory: @Sendable () -> String
var createDirectory: @Sendable (String, Bool, [FileAttributeKey : Any]?) throws -> Void
var fileExists: @Sendable (String) -> Bool
var removeItem: @Sendable (_ path: String) throws -> Void
var workingDirectory: @Sendable () -> String

// pass-through methods to preserve argument labels
func attributesOfItem(atPath path: String) throws -> [FileAttributeKey : Any] {
try attributesOfItem(path)
}
func contents(atPath path: String) -> Data? { contents(path) }
func contentsOfDirectory(atPath path: String) throws -> [String] {
try contentsOfDirectory(path)
}
func createDirectory(atPath path: String,
withIntermediateDirectories createIntermediates: Bool,
attributes: [FileAttributeKey : Any]?) throws {
Expand All @@ -71,10 +60,6 @@ struct FileManager: Sendable {
func removeItem(atPath path: String) throws { try removeItem(path) }

static let live: Self = .init(
attributesOfItem: { try Foundation.FileManager.default.attributesOfItem(atPath: $0) },
contentsOfDirectory: { try Foundation.FileManager.default.contentsOfDirectory(atPath: $0) },
contents: { Foundation.FileManager.default.contents(atPath: $0) },
checkoutsDirectory: { Environment.get("CHECKOUTS_DIR") ?? DirectoryConfiguration.detect().workingDirectory + "SPI-checkouts" },
createDirectory: { try Foundation.FileManager.default.createDirectory(atPath: $0, withIntermediateDirectories: $1, attributes: $2) },
fileExists: { Foundation.FileManager.default.fileExists(atPath: $0) },
removeItem: { try Foundation.FileManager.default.removeItem(atPath: $0) },
Expand All @@ -83,14 +68,6 @@ struct FileManager: Sendable {
}


extension FileManager {
func cacheDirectoryPath(for package: Package) -> String? {
guard let dirname = package.cacheDirectoryName else { return nil }
return checkoutsDirectory() + "/" + dirname
}
}


struct Git: Sendable {
var commitCount: @Sendable (String) async throws -> Int
var firstCommitDate: @Sendable (String) async throws -> Date
Expand Down
67 changes: 67 additions & 0 deletions Sources/App/Core/Dependencies/FileManagerClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

import Dependencies
import DependenciesMacros
import IssueReporting
import Vapor


@DependencyClient
struct FileManagerClient {
var attributesOfItem: @Sendable (_ atPath: String) throws -> [FileAttributeKey : Any]
var checkoutsDirectory: @Sendable () -> String = { reportIssue("checkoutsDirectory"); return "SPI-checkouts" }
var contents: @Sendable (_ atPath: String) -> Data?
var contentsOfDirectory: @Sendable (_ atPath: String) throws -> [String]
}


extension FileManagerClient {
func cacheDirectoryPath(for package: Package) -> String? {
guard let dirname = package.cacheDirectoryName else { return nil }
return checkoutsDirectory() + "/" + dirname
}
}


extension FileManagerClient: DependencyKey {
static var liveValue: Self {
.init(
attributesOfItem: { try Foundation.FileManager.default.attributesOfItem(atPath: $0) },
checkoutsDirectory: { Environment.get("CHECKOUTS_DIR") ?? DirectoryConfiguration.detect().workingDirectory + "SPI-checkouts" },
contents: { Foundation.FileManager.default.contents(atPath: $0) },
contentsOfDirectory: { try Foundation.FileManager.default.contentsOfDirectory(atPath: $0) }
)
}
}


extension FileManagerClient: TestDependencyKey {
static var testValue: Self {
var mock = Self()
// Override the `unimplemented` default because it is a very common dependency.
mock.checkoutsDirectory = { "SPI-checkouts" }
return mock
}
}


extension DependencyValues {
var fileManager: FileManagerClient {
get { self[FileManagerClient.self] }
set { self[FileManagerClient.self] = newValue }
}
}
6 changes: 4 additions & 2 deletions Sources/App/Views/Blog/BlogActions+Model.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ enum BlogActions {
}

static func allSummaries() throws -> [PostSummary] {
guard let data = Current.fileManager.contents(atPath: Self.blogIndexYmlPath) else {
@Dependency(\.fileManager) var fileManager
guard let data = fileManager.contents(atPath: Self.blogIndexYmlPath) else {
throw AppError.genericError(nil, "failed to read posts.yml")
}

Expand All @@ -78,11 +79,12 @@ enum BlogActions {
extension BlogActions.Model.PostSummary {

var postMarkdown: String {
@Dependency(\.fileManager) var fileManager
let markdownPath = Current.fileManager.workingDirectory()
.appending("Resources/Blog/Posts/")
.appending(slug)
.appending(".md")
if let markdownData = Current.fileManager.contents(atPath: markdownPath),
if let markdownData = fileManager.contents(atPath: markdownPath),
let markdown = String(data: markdownData, encoding: .utf8)
{
let parsedMarkdown = MarkdownParser().parse(markdown)
Expand Down
12 changes: 8 additions & 4 deletions Sources/App/Views/ReadyForSwift6/ReadyForSwift6Show+Model.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
// limitations under the License.

import Foundation

import Dependencies
import Plot


enum ReadyForSwift6Show {

struct Model {
Expand All @@ -25,11 +28,12 @@ enum ReadyForSwift6Show {
}

func readyForSwift6Chart(kind: ChartKind, includeTotals: Bool = false) -> Node<HTML.BodyContext> {
@Dependency(\.fileManager) var fileManager
let plotDataPath = Current.fileManager.workingDirectory().appending("Resources/ChartData/\(kind.dataFile)")
let eventDataPath = Current.fileManager.workingDirectory().appending("Resources/ChartData/rfs6-events.json")
guard let plotData = Current.fileManager.contents(atPath: plotDataPath)?.compactJson(),
let eventData = Current.fileManager.contents(atPath: eventDataPath)?.compactJson()
else { return .p("Couldn’t load chart data.") }
guard let plotData = fileManager.contents(atPath: plotDataPath)?.compactJson(),
let eventData = fileManager.contents(atPath: eventDataPath)?.compactJson()
else { return .p("Couldn’t load chart data.") }

return .div(
.data(named: "controller", value: "vega-chart"),
Expand Down Expand Up @@ -73,7 +77,7 @@ private extension ReadyForSwift6Show.Model.ChartKind {
private extension Data {
func compactJson() -> String? {
guard let json = try? JSONSerialization.jsonObject(with: self),
let compactedJsonData = try? JSONSerialization.data(withJSONObject: json),
let compactedJsonData = try? JSONSerialization.data(withJSONObject: json, options: [.sortedKeys]),
let compactJson = String(data: compactedJsonData, encoding: .utf8)
else { return nil }
return compactJson
Expand Down
3 changes: 2 additions & 1 deletion Sources/App/Views/ResourceReloadIdentifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ struct ResourceReloadIdentifier {

private static func modificationDate(forLocalResource resource: String) -> Date {
@Dependency(\.date.now) var now
@Dependency(\.fileManager) var fileManager
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)
guard let attributes = try? fileManager.attributesOfItem(atPath: url.path)
else { return now }

// Also assume the file is modified now if the attribute doesn't exist.
Expand Down
39 changes: 12 additions & 27 deletions Tests/AppTests/AnalyzerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -507,11 +507,7 @@ class AnalyzerTests: AppTestCase {
Current.fileManager.fileExists = { @Sendable _ in true }
let commands = QueueIsolated<[String]>([])
Current.shell.run = { @Sendable cmd, path in
// mask variable checkout
let checkoutDir = Current.fileManager.checkoutsDirectory()
commands.withValue {
$0.append(cmd.description.replacingOccurrences(of: checkoutDir, with: "..."))
}
commands.withValue { $0.append(cmd.description) }
return ""
}
let jpr = try await Package.fetchCandidate(app.db, id: pkg.id!)
Expand Down Expand Up @@ -961,17 +957,13 @@ class AnalyzerTests: AppTestCase {
try await savePackage(on: app.db, "1".asGithubUrl.url, processingStage: .ingestion)
let pkgs = try await Package.fetchCandidates(app.db, for: .analysis, limit: 10)

let checkoutDir = Current.fileManager.checkoutsDirectory()
// claim every file exists, including our ficticious 'index.lock' for which
// we want to trigger the cleanup mechanism
Current.fileManager.fileExists = { @Sendable path in true }

let commands = QueueIsolated<[String]>([])
Current.shell.run = { @Sendable cmd, path in
commands.withValue {
let c = cmd.description.replacingOccurrences(of: checkoutDir, with: "...")
$0.append(c)
}
commands.withValue { $0.append(cmd.description) }
return ""
}

Expand All @@ -995,17 +987,13 @@ class AnalyzerTests: AppTestCase {
try await savePackage(on: app.db, "1".asGithubUrl.url, processingStage: .ingestion)
let pkgs = try await Package.fetchCandidates(app.db, for: .analysis, limit: 10)

let checkoutDir = Current.fileManager.checkoutsDirectory()
// claim every file exists, including our ficticious 'index.lock' for which
// we want to trigger the cleanup mechanism
Current.fileManager.fileExists = { @Sendable path in true }

let commands = QueueIsolated<[String]>([])
Current.shell.run = { @Sendable cmd, path in
commands.withValue {
let c = cmd.description.replacingOccurrences(of: checkoutDir, with: "${checkouts}")
$0.append(c)
}
commands.withValue { $0.append(cmd.description) }
if cmd == .gitCheckout(branch: "master") {
throw TestError.simulatedCheckoutError
}
Expand Down Expand Up @@ -1155,11 +1143,7 @@ class AnalyzerTests: AppTestCase {
Current.fileManager.fileExists = { @Sendable _ in true }
let commands = QueueIsolated<[String]>([])
Current.shell.run = { @Sendable cmd, _ in
commands.withValue {
// mask variable checkout
let checkoutDir = Current.fileManager.checkoutsDirectory()
$0.append(cmd.description.replacingOccurrences(of: checkoutDir, with: "..."))
}
commands.withValue { $0.append(cmd.description) }
if cmd == .gitFetchAndPruneTags { throw TestError.simulatedFetchError }
return ""
}
Expand Down Expand Up @@ -1235,6 +1219,7 @@ class AnalyzerTests: AppTestCase {
// Ensure we handle 404 repos properly
// https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/914
// setup
let checkoutDir = "/checkouts"
do {
let url = "1".asGithubUrl.url
let pkg = Package.init(url: url, processingStage: .ingestion)
Expand All @@ -1243,13 +1228,13 @@ class AnalyzerTests: AppTestCase {
if path.hasSuffix("github.com-foo-1") { return false }
return true
}
let repoDir = try Current.fileManager.checkoutsDirectory() + "/" + XCTUnwrap(pkg.cacheDirectoryName)
let repoDir = try checkoutDir + "/" + XCTUnwrap(pkg.cacheDirectoryName)
struct ShellOutError: Error {}
Current.shell.run = { @Sendable cmd, path in
if cmd == .gitClone(url: url, to: repoDir) {
throw ShellOutError()
}
fatalError("should not be reached")
throw TestError.unknownCommand
}
}
let lastUpdated = Date()
Expand All @@ -1268,16 +1253,16 @@ class AnalyzerTests: AppTestCase {
func test_trimCheckouts() throws {
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
$0.fileManager.attributesOfItem = { @Sendable path in
[
"/checkouts/foo": [FileAttributeKey.modificationDate: Date.t0.adding(days: -31)],
"/checkouts/bar": [FileAttributeKey.modificationDate: Date.t0.adding(days: -29)],
][path]!
}
$0.fileManager.checkoutsDirectory = { "/checkouts" }
$0.fileManager.contentsOfDirectory = { @Sendable _ in ["foo", "bar"] }
} operation: {
// setup
let removedPaths = NIOLockedValueBox<[String]>([])
Current.fileManager.removeItem = { @Sendable p in removedPaths.withLockedValue { $0.append(p) } }

Expand Down
Loading
Loading