From d6c06167f645f2cc95ef5d841c405305662e3966 Mon Sep 17 00:00:00 2001 From: Drew McCormack Date: Sun, 15 Dec 2024 17:46:25 +0100 Subject: [PATCH] Fixes to FileRepository to get it to compile and pass tests. --- Sources/Forked/FileRepository.swift | 94 +++++++++--- Sources/Forked/Fork.swift | 6 +- Tests/ForkedTests/FileRepository.swift | 162 +++++++++++++++++++++ Tests/ForkedTests/FileRepositoryTest.swift | 63 -------- 4 files changed, 236 insertions(+), 89 deletions(-) create mode 100644 Tests/ForkedTests/FileRepository.swift delete mode 100644 Tests/ForkedTests/FileRepositoryTest.swift diff --git a/Sources/Forked/FileRepository.swift b/Sources/Forked/FileRepository.swift index 095959c6..755c311f 100644 --- a/Sources/Forked/FileRepository.swift +++ b/Sources/Forked/FileRepository.swift @@ -15,7 +15,7 @@ public final class FileRepository: Repository { guard let subdirectories = try? FileManager.default.contentsOfDirectory(at: rootDirectory, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) else { return [] } - return subdirectories.filter { $0.hasDirectoryPath }.map { Fork($0.lastPathComponent) } + return subdirectories.filter { $0.hasDirectoryPath }.map { Fork(name: $0.lastPathComponent) } } public func create(_ fork: Fork, withInitialCommit commit: Commit) throws { @@ -35,45 +35,93 @@ public final class FileRepository: Repository { try FileManager.default.removeItem(at: forkDirectory) } - public func versions(storedIn fork: Fork) throws -> Set { - let forkDirectory = rootDirectory.appendingPathComponent(fork.name) - guard FileManager.default.fileExists(atPath: forkDirectory.path) else { - throw Error.attemptToAccessNonExistentFork(fork) - } - let files = try FileManager.default.contentsOfDirectory(at: forkDirectory, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) - return Set(files.map { Version($0.lastPathComponent) }) + private struct VersionMetadata: Codable { + let count: UInt64 + let timestamp: Date + } + + private func metadataURL(for version: Version, in fork: Fork) -> URL { + rootDirectory + .appendingPathComponent(fork.name) + .appendingPathComponent("\(version.count).metadata") + } + + private func dataURL(for version: Version, in fork: Fork) -> URL { + rootDirectory + .appendingPathComponent(fork.name) + .appendingPathComponent(String(version.count)) } - public func removeCommit(at version: Version, from fork: Fork) throws { + public func content(of fork: Fork, at version: Version) throws -> CommitContent { let forkDirectory = rootDirectory.appendingPathComponent(fork.name) - let fileURL = forkDirectory.appendingPathComponent(version.id) - guard FileManager.default.fileExists(atPath: fileURL.path) else { + let dataURL = forkDirectory.appendingPathComponent(String(version.count)) + let metadataURL = forkDirectory.appendingPathComponent("\(version.count).metadata") + + guard FileManager.default.fileExists(atPath: metadataURL.path) else { throw Error.attemptToAccessNonExistentVersion(version, fork) } - try FileManager.default.removeItem(at: fileURL) + + if FileManager.default.fileExists(atPath: dataURL.path) { + return .resource(try Data(contentsOf: dataURL)) + } else { + return .none + } } - public func content(of fork: Fork, at version: Version) throws -> Data { + public func store(_ commit: Commit, in fork: Fork) throws { let forkDirectory = rootDirectory.appendingPathComponent(fork.name) - let fileURL = forkDirectory.appendingPathComponent(version.id) - guard FileManager.default.fileExists(atPath: fileURL.path) else { - throw Error.attemptToAccessNonExistentVersion(version, fork) + guard FileManager.default.fileExists(atPath: forkDirectory.path) else { + throw Error.attemptToAccessNonExistentFork(fork) + } + + let dataURL = forkDirectory.appendingPathComponent(String(commit.version.count)) + let metadataURL = forkDirectory.appendingPathComponent("\(commit.version.count).metadata") + + guard !FileManager.default.fileExists(atPath: metadataURL.path) else { + throw Error.attemptToReplaceExistingVersion(commit.version, fork) + } + + // Save metadata + let metadata = VersionMetadata(count: commit.version.count, timestamp: commit.version.timestamp) + let encoder = JSONEncoder() + try encoder.encode(metadata).write(to: metadataURL) + + // Save content if it's not .none + if case .resource(let data) = commit.content { + try data.write(to: dataURL) } - return try Data(contentsOf: fileURL) } - public func store(_ commit: Commit, in fork: Fork) throws { + public func versions(storedIn fork: Fork) throws -> Set { let forkDirectory = rootDirectory.appendingPathComponent(fork.name) guard FileManager.default.fileExists(atPath: forkDirectory.path) else { throw Error.attemptToAccessNonExistentFork(fork) } - let fileURL = forkDirectory.appendingPathComponent(commit.version.id) - guard !FileManager.default.fileExists(atPath: fileURL.path) else { - throw Error.attemptToReplaceExistingVersion(commit.version, fork) + + let files = try FileManager.default.contentsOfDirectory(at: forkDirectory, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) + + let decoder = JSONDecoder() + return Set(files.compactMap { url in + guard url.pathExtension == "metadata" else { return nil } + guard let metadata = try? decoder.decode(VersionMetadata.self, from: Data(contentsOf: url)) else { return nil } + return Version(count: metadata.count, timestamp: metadata.timestamp) + }) + } + + public func removeCommit(at version: Version, from fork: Fork) throws { + let forkDirectory = rootDirectory.appendingPathComponent(fork.name) + let dataURL = forkDirectory.appendingPathComponent(String(version.count)) + let metadataURL = forkDirectory.appendingPathComponent("\(version.count).metadata") + + guard FileManager.default.fileExists(atPath: metadataURL.path) else { + throw Error.attemptToAccessNonExistentVersion(version, fork) + } + + try FileManager.default.removeItem(at: metadataURL) + if FileManager.default.fileExists(atPath: dataURL.path) { + try FileManager.default.removeItem(at: dataURL) } - try commit.content.write(to: fileURL) } - private func createDirectoryIfNeeded(at url: URL) throws { if !FileManager.default.fileExists(atPath: url.path) { diff --git a/Sources/Forked/Fork.swift b/Sources/Forked/Fork.swift index 2b7a5c22..9fd84400 100644 --- a/Sources/Forked/Fork.swift +++ b/Sources/Forked/Fork.swift @@ -1,6 +1,6 @@ import Foundation -/// A type representing a named fork. +/// A type representing a named fork. public struct Fork: Hashable, Codable, Sendable { /// The name of the fork public let name: String @@ -14,9 +14,9 @@ public struct Fork: Hashable, Codable, Sendable { self.name = name } - /// The only fork created by default. All other forkes + /// The only fork created by default. All other forks /// can be merged with the main, but not directly with - /// each other.. It acts as the central hub of the + /// each other. It acts as the central hub of the /// wheel of forks. public static let main = Fork(name: "main") } diff --git a/Tests/ForkedTests/FileRepository.swift b/Tests/ForkedTests/FileRepository.swift new file mode 100644 index 00000000..020d7da7 --- /dev/null +++ b/Tests/ForkedTests/FileRepository.swift @@ -0,0 +1,162 @@ +import Foundation +import Testing +@testable import Forked + +struct FileRepositorySuite { + @Test func createAndListForks() throws { + let tempDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: tempDirectory) } + + let repository = try FileRepository(rootDirectory: tempDirectory) + + // Initially no forks + #expect(repository.forks.isEmpty) + + // Create a fork + let fork = Fork(name: "test") + let initialCommit = Commit(content: .resource(Data()), version: .initialVersion) + try repository.create(fork, withInitialCommit: initialCommit) + + // Check fork exists + #expect(repository.forks.count == 1) + #expect(repository.forks.first?.name == "test") + } + + @Test func storeAndRetrieveContent() throws { + let tempDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: tempDirectory) } + + let repository = try FileRepository(rootDirectory: tempDirectory) + let fork = Fork(name: "test") + let initialCommit = Commit(content: .resource(Data()), version: .initialVersion) + try repository.create(fork, withInitialCommit: initialCommit) + + // Store new content + let testData = "Hello, World!".data(using: .utf8)! + let version = Version(count: 1, timestamp: .now) + let commit = Commit(content: .resource(testData), version: version) + try repository.store(commit, in: fork) + + // Retrieve and verify content + let retrieved = try repository.content(of: fork, at: version) + #expect(retrieved == .resource(testData)) + } + + @Test func versionListing() throws { + let tempDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: tempDirectory) } + + let repository = try FileRepository(rootDirectory: tempDirectory) + let fork = Fork(name: "test") + let initialCommit = Commit(content: .resource(Data()), version: .initialVersion) + try repository.create(fork, withInitialCommit: initialCommit) + + // Check initial version + var versions = try repository.versions(storedIn: fork) + #expect(versions.count == 1) + #expect(versions.contains(.initialVersion)) + + // Add another version + let version = Version(count: 1, timestamp: .now) + let commit = Commit(content: .resource(Data()), version: version) + try repository.store(commit, in: fork) + + // Check both versions exist + versions = try repository.versions(storedIn: fork) + #expect(versions.count == 2) + #expect(versions.contains(.initialVersion)) + #expect(versions.contains(version)) + } + + @Test func deleteFork() throws { + let tempDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: tempDirectory) } + + let repository = try FileRepository(rootDirectory: tempDirectory) + let fork = Fork(name: "test") + let initialCommit = Commit(content: .resource(Data()), version: .initialVersion) + try repository.create(fork, withInitialCommit: initialCommit) + + #expect(repository.forks.count == 1) + try repository.delete(fork) + #expect(repository.forks.isEmpty) + } + + @Test func removeCommit() throws { + let tempDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: tempDirectory) } + + let repository = try FileRepository(rootDirectory: tempDirectory) + let fork = Fork(name: "test") + let initialCommit = Commit(content: .resource(Data()), version: .initialVersion) + try repository.create(fork, withInitialCommit: initialCommit) + + let version = Version(count: 1, timestamp: .now) + let commit = Commit(content: .resource(Data()), version: version) + try repository.store(commit, in: fork) + + var versions = try repository.versions(storedIn: fork) + #expect(versions.count == 2) + + try repository.removeCommit(at: version, from: fork) + + versions = try repository.versions(storedIn: fork) + #expect(versions.count == 1) + #expect(versions.contains(.initialVersion)) + } + + @Test func noneContent() throws { + let tempDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: tempDirectory) } + + let repository = try FileRepository(rootDirectory: tempDirectory) + let fork = Fork(name: "test") + let initialCommit = Commit(content: .none, version: .initialVersion) + try repository.create(fork, withInitialCommit: initialCommit) + + let version = Version(count: 1, timestamp: .now) + let commit = Commit(content: .none, version: version) + try repository.store(commit, in: fork) + + let retrieved = try repository.content(of: fork, at: version) + #expect(retrieved == .none) + } + + @Test func errorConditions() throws { + let tempDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: tempDirectory) } + + let repository = try FileRepository(rootDirectory: tempDirectory) + let fork = Fork(name: "test") + let nonExistentFork = Fork(name: "nonexistent") + let version = Version(count: 1, timestamp: .now) + + // Accessing non-existent fork + #expect(throws: Error.self) { + try repository.versions(storedIn: nonExistentFork) + } + #expect(throws: Error.self) { + try repository.content(of: nonExistentFork, at: version) + } + + let initialCommit = Commit(content: .resource(Data()), version: .initialVersion) + try repository.create(fork, withInitialCommit: initialCommit) + + // Duplicate fork creation + #expect(throws: Error.self) { + try repository.create(fork, withInitialCommit: initialCommit) + } + + // Accessing non-existent version + #expect(throws: Error.self) { + try repository.content(of: fork, at: version) + } + + // Store commit and try to replace it + let commit = Commit(content: .resource(Data()), version: version) + try repository.store(commit, in: fork) + #expect(throws: Error.self) { + try repository.store(commit, in: fork) + } + } +} diff --git a/Tests/ForkedTests/FileRepositoryTest.swift b/Tests/ForkedTests/FileRepositoryTest.swift deleted file mode 100644 index d41c16c6..00000000 --- a/Tests/ForkedTests/FileRepositoryTest.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Foundation -import Testing -@testable import Forked - -struct FileRepositoryTests { - - let testDirectoryURL = URL(fileURLWithPath: "/tmp/repository") - - @Test func savingBasicFileRepository() throws { - try? FileManager.default.removeItem(at: testDirectoryURL) - let repo = try FileRepository(rootDirectory: testDirectoryURL) - #expect(repo.forks.isEmpty) - let fork = Fork("main") - let commit = Commit(version: Version("v1"), content: "Hello, Forked!".data(using: .utf8)!) - try repo.create(fork, withInitialCommit: commit) - #expect(repo.forks == [fork]) - let retrievedContent = try repo.content(of: fork, at: commit.version) - let retrievedString = String(data: retrievedContent, encoding: .utf8) - #expect(retrievedString == "Hello, Forked!") - } - - @Test func savingFileRepositoryWithChanges() throws { - try? FileManager.default.removeItem(at: testDirectoryURL) - let repo = try FileRepository(rootDirectory: testDirectoryURL) - let fork = Fork("main") - let commit1 = Commit(version: Version("v1"), content: "First commit".data(using: .utf8)!) - let commit2 = Commit(version: Version("v2"), content: "Second commit".data(using: .utf8)!) - try repo.create(fork, withInitialCommit: commit1) - try repo.store(commit2, in: fork) - let versions = try repo.versions(storedIn: fork) - #expect(versions.count == 2) - let retrievedContent = try repo.content(of: fork, at: commit2.version) - let retrievedString = String(data: retrievedContent, encoding: .utf8) - #expect(retrievedString == "Second commit") - } - - @Test func deletingFork() throws { - try? FileManager.default.removeItem(at: testDirectoryURL) - let repo = try FileRepository(rootDirectory: testDirectoryURL) - let fork = Fork("main") - let commit = Commit(version: Version("v1"), content: "Initial commit".data(using: .utf8)!) - try repo.create(fork, withInitialCommit: commit) - #expect(repo.forks.contains(fork)) - try repo.delete(fork) - #expect(repo.forks.isEmpty) - } - - @Test func deletingCommit() throws { - try? FileManager.default.removeItem(at: testDirectoryURL) - let repo = try FileRepository(rootDirectory: testDirectoryURL) - let fork = Fork("main") - let commit1 = Commit(version: Version("v1"), content: "First commit".data(using: .utf8)!) - let commit2 = Commit(version: Version("v2"), content: "Second commit".data(using: .utf8)!) - try repo.create(fork, withInitialCommit: commit1) - try repo.store(commit2, in: fork) - let versions = try repo.versions(storedIn: fork) - #expect(versions.count == 2) - try repo.removeCommit(at: commit1.version, from: fork) - let versionsAfterDeletion = try repo.versions(storedIn: fork) - #expect(versionsAfterDeletion.count == 1) - #expect(versionsAfterDeletion.first?.id == commit2.version.id) - } -} \ No newline at end of file