diff --git a/Tests/SwiftGitX.xctestplan b/Tests/SwiftGitX.xctestplan deleted file mode 100644 index 70a4799..0000000 --- a/Tests/SwiftGitX.xctestplan +++ /dev/null @@ -1,27 +0,0 @@ -{ - "configurations" : [ - { - "id" : "B08F7335-0FFC-46A5-AC8B-B4FF20AF033D", - "name" : "Test Scheme Action", - "options" : { - - } - } - ], - "defaultOptions" : { - "testRepetitionMode" : "retryOnFailure" - }, - "testTargets" : [ - { - "skippedTests" : [ - "SwiftGitXTestCase" - ], - "target" : { - "containerPath" : "container:", - "identifier" : "SwiftGitXTests", - "name" : "SwiftGitXTests" - } - } - ], - "version" : 1 -} diff --git a/Tests/SwiftGitXTests/CollectionTests/BranchCollectionTests.swift b/Tests/SwiftGitXTests/CollectionTests/BranchCollectionTests.swift index 83ca706..ad53c68 100644 --- a/Tests/SwiftGitXTests/CollectionTests/BranchCollectionTests.swift +++ b/Tests/SwiftGitXTests/CollectionTests/BranchCollectionTests.swift @@ -1,163 +1,185 @@ +import Foundation import SwiftGitX -import XCTest +import Testing -final class BranchCollectionTests: SwiftGitXTestCase { - func testBranchLookup() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-branch-lookup", in: Self.directory) - - // Create mock commit +@Suite("Branch Collection", .tags(.branch, .collection)) +final class BranchCollectionTests: SwiftGitXTest { + @Test("Lookup branch by name") + func branchLookup() async throws { + let repository = mockRepository() let commit = try repository.mockCommit() - // Lookup the branch let lookupBranch = try repository.branch.get(named: "main", type: .local) - // Check the branch - XCTAssertEqual(lookupBranch.name, "main") - XCTAssertEqual(lookupBranch.fullName, "refs/heads/main") - XCTAssertEqual(lookupBranch.target.id, commit.id) + #expect(lookupBranch.name == "main") + #expect(lookupBranch.fullName == "refs/heads/main") + #expect(lookupBranch.target.id == commit.id) } - func testBranchLookupSubscript() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-branch-lookup-subscript", in: Self.directory) - - // Create mock commit + @Test("Lookup branch using subscript") + func branchLookupSubscript() async throws { + let repository = mockRepository() let commit = try repository.mockCommit() - // Lookup the branch - let lookupBranch = try XCTUnwrap(repository.branch["main"]) - let lookupBranchLocal = try XCTUnwrap(repository.branch["main", type: .local]) - - XCTAssertEqual(lookupBranch, lookupBranchLocal) + let lookupBranch = try #require(repository.branch["main"]) + let lookupBranchLocal = try #require(repository.branch["main", type: .local]) - // Check the branch - XCTAssertEqual(lookupBranch.name, "main") - XCTAssertEqual(lookupBranch.fullName, "refs/heads/main") - XCTAssertEqual(lookupBranch.target.id, commit.id) + #expect(lookupBranch == lookupBranchLocal) + #expect(lookupBranch.name == "main") + #expect(lookupBranch.fullName == "refs/heads/main") + #expect(lookupBranch.target.id == commit.id) // Lookup remote branch (should be nil) let lookupBranchRemote = repository.branch["main", type: .remote] - XCTAssertNil(lookupBranchRemote) + #expect(lookupBranchRemote == nil) } - func testBranchCurrent() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-branch-current", in: Self.directory) - - // Create mock commit + @Test("Get current branch") + func branchCurrent() async throws { + let repository = mockRepository() try repository.mockCommit() - // Get the current branch - let currentBranch = try XCTUnwrap(repository.branch.current) + let currentBranch = try repository.branch.current - // Check the current branch - XCTAssertEqual(currentBranch.name, "main") - XCTAssertEqual(currentBranch.fullName, "refs/heads/main") - XCTAssertEqual(currentBranch.type, .local) + #expect(currentBranch.name == "main") + #expect(currentBranch.fullName == "refs/heads/main") + #expect(currentBranch.type == .local) } - func testBranchCreate() throws { - let repository = Repository.mock(named: "test-branch-create", in: Self.directory) - - // Create mock commit + @Test("Create new branch") + func branchCreate() async throws { + let repository = mockRepository() let commit = try repository.mockCommit() - // Create a new branch let branch = try repository.branch.create(named: "develop", target: commit) - // Check the branch - XCTAssertEqual(branch.name, "develop") - XCTAssertEqual(branch.fullName, "refs/heads/develop") - XCTAssertEqual(branch.target.id, commit.id) - XCTAssertEqual(branch.type, .local) + #expect(branch.name == "develop") + #expect(branch.fullName == "refs/heads/develop") + #expect(branch.target.id == commit.id) + #expect(branch.type == .local) } - func testBranchCreateFrom() throws { - let repository = Repository.mock(named: "test-branch-create-from", in: Self.directory) - - // Create mock commit + @Test("Create branch from another branch") + func branchCreateFrom() async throws { + let repository = mockRepository() try repository.mockCommit() - // Get the main branch let mainBranch = try repository.branch.get(named: "main") - - // Create a new branch let newBranch = try repository.branch.create(named: "develop", from: mainBranch) - // Check the branch - XCTAssertEqual(newBranch.name, "develop") - XCTAssertEqual(newBranch.fullName, "refs/heads/develop") - XCTAssertEqual(newBranch.target.id, mainBranch.target.id) - XCTAssertEqual(newBranch.type, .local) + #expect(newBranch.name == "develop") + #expect(newBranch.fullName == "refs/heads/develop") + #expect(newBranch.target.id == mainBranch.target.id) + #expect(newBranch.type == .local) } - func testBranchDelete() throws { - let repository = Repository.mock(named: "test-branch-delete", in: Self.directory) + @Test("Create branch with force flag overwrites existing") + func branchCreateForce() async throws { + let repository = mockRepository() + let commit1 = try repository.mockCommit() - // Create mock commit - let commit: Commit = try repository.mockCommit() + // Create initial branch + let branch1 = try repository.branch.create(named: "develop", target: commit1) + #expect(branch1.target.id == commit1.id) + + // Create another commit + let commit2 = try repository.mockCommit(message: "Second commit") + + // Try to create branch without force (should fail) + #expect(throws: SwiftGitXError.self) { + try repository.branch.create(named: "develop", target: commit2, force: false) + } + + // Create branch with force (should succeed and point to new commit) + let branch2 = try repository.branch.create(named: "develop", target: commit2, force: true) + #expect(branch2.name == "develop") + #expect(branch2.target.id == commit2.id) + #expect(branch2.target.id != commit1.id) + } + + @Test("Delete branch") + func branchDelete() async throws { + let repository = mockRepository() + let commit = try repository.mockCommit() - // Create a new branch let branch = try repository.branch.create(named: "develop", target: commit) - // Delete the branch - XCTAssertNoThrow(try repository.branch.delete(branch)) + try repository.branch.delete(branch) - // Check the branch - XCTAssertThrowsError(try repository.branch.get(named: "develop")) - XCTAssertNil(repository.branch["develop"]) + #expect(throws: SwiftGitXError.self) { + try repository.branch.get(named: "develop") + } + #expect(repository.branch["develop"] == nil) - // Check the current branch - XCTAssertEqual(try repository.branch.current.name, "main") + // Check the current branch is still main + #expect(try repository.branch.current.name == "main") } - func testBranchDeleteCurrentFailure() throws { - let repository = Repository.mock(named: "test-branch-delete-current-failure", in: Self.directory) - - // Create mock commit + @Test("Delete current branch fails") + func branchDeleteCurrentFailure() async throws { + let repository = mockRepository() try repository.mockCommit() - // Get the main branch (current branch) let mainBranch = try repository.branch.get(named: "main") - // Delete the branch - XCTAssertThrowsError(try repository.branch.delete(mainBranch)) + #expect(throws: SwiftGitXError.self) { + try repository.branch.delete(mainBranch) + } } - func testBranchRename() throws { - let repository = Repository.mock(named: "test-branch-rename", in: Self.directory) - - // Create mock commit + @Test("Rename branch") + func branchRename() async throws { + let repository = mockRepository() let commit = try repository.mockCommit() - // Create a new branch let branch = try repository.branch.create(named: "develop", target: commit) - - // Rename the branch let newBranch = try repository.branch.rename(branch, to: "feature") - // Check the branch - XCTAssertEqual(newBranch.name, "feature") - XCTAssertEqual(newBranch.fullName, "refs/heads/feature") - XCTAssertEqual(newBranch.target.id, commit.id) - XCTAssertEqual(newBranch.type, .local) + #expect(newBranch.name == "feature") + #expect(newBranch.fullName == "refs/heads/feature") + #expect(newBranch.target.id == commit.id) + #expect(newBranch.type == .local) - // Check the old branch - XCTAssertThrowsError(try repository.branch.get(named: "develop")) - XCTAssertNil(repository.branch["develop"]) + // Check the old branch no longer exists + #expect(throws: SwiftGitXError.self) { + try repository.branch.get(named: "develop") + } + #expect(repository.branch["develop"] == nil) } - func testBranchSequenceLocal() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-branch-sequence-local", in: Self.directory) + @Test("Rename branch with force flag overwrites existing") + func branchRenameForce() async throws { + let repository = mockRepository() + let commit = try repository.mockCommit() + + // Create two branches + let branch1 = try repository.branch.create(named: "develop", target: commit) + _ = try repository.branch.create(named: "feature", target: commit) - // Get the local branches - // (must be empty because the main branch is unborn) - let localBranchesEmpty = Array(repository.branch.local) + // Try to rename without force (should fail) + #expect(throws: SwiftGitXError.self) { + try repository.branch.rename(branch1, to: "feature", force: false) + } - // Check empty branches - XCTAssertEqual(localBranchesEmpty, []) + // Rename with force (should succeed) + let renamedBranch = try repository.branch.rename(branch1, to: "feature", force: true) + #expect(renamedBranch.name == "feature") + + // Check the old branch no longer exists + #expect(repository.branch["develop"] == nil) + + // The original feature branch should be overwritten + let featureBranch = try repository.branch.get(named: "feature") + #expect(featureBranch.target.id == commit.id) + } + + @Test("Iterate local branches") + func branchSequenceLocal() async throws { + let repository = mockRepository() + + // Get the local branches (must be empty because the main branch is unborn) + let localBranchesEmpty = Array(repository.branch.local) + #expect(localBranchesEmpty.isEmpty) // Create mock commit let commit = try repository.mockCommit() @@ -172,33 +194,29 @@ final class BranchCollectionTests: SwiftGitXTestCase { let localBranches = Array(repository.branch.local) // Check the local branches count (including the main branch) - XCTAssertEqual(localBranches.count, 5) + #expect(localBranches.count == 5) // Check the local branches let allBranchNames = repository.branch.local.map(\.name) for name in allBranchNames { let branch = try repository.branch.get(named: name, type: .local) - XCTAssertTrue(localBranches.contains(branch)) + #expect(localBranches.contains(branch)) } } - func testBranchListLocal() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-branch-list-local", in: Self.directory) + @Test("List local branches") + func branchListLocal() async throws { + let repository = mockRepository() - // Get the local branches - // (must be empty because the main branch is unborn) + // Get the local branches (must be empty because the main branch is unborn) let branches = try repository.branch.list(.local) - - // Check empty branches - XCTAssertEqual(branches, []) + #expect(branches.isEmpty) // Create a new commit let commit = try repository.mockCommit() // Create some new branches let newBranchNames = ["other-branch", "another-branch", "one-more-branch", "last-branch"] - for name in newBranchNames { try repository.branch.create(named: name, target: commit) } @@ -206,65 +224,269 @@ final class BranchCollectionTests: SwiftGitXTestCase { // Get the local branches let localBranches = try repository.branch.list(.local) - // Check the local branches count - XCTAssertEqual(localBranches.count, 5) + // Check the local branches count (including the main branch) + #expect(localBranches.count == 5) - // Check the local branches (we need to check main branch too) + // Check the local branches let allBranchNames = localBranches.map(\.name) for name in allBranchNames { let branch = try repository.branch.get(named: name, type: .local) - XCTAssertTrue(localBranches.contains(branch)) + #expect(localBranches.contains(branch)) + } + } + + @Test("List all branches") + func branchListAll() async throws { + let repository = mockRepository() + + // Create a new commit + let commit = try repository.mockCommit() + + // Create some new branches + let newBranchNames = ["develop", "feature"] + for name in newBranchNames { + try repository.branch.create(named: name, target: commit) + } + + // Get all branches (default parameter) + let allBranches = try repository.branch.list() + let allBranchesExplicit = try repository.branch.list(.all) + + // Both should be equal + #expect(allBranches.count == allBranchesExplicit.count) + #expect(allBranches.count == 3) // main, develop, feature + + // Check all branch types are local (since no remote yet) + for branch in allBranches { + #expect(branch.type == .local) + } + } + + @Test("Iterate all branches") + func branchIterateAll() async throws { + let repository = mockRepository() + + // Create a new commit + let commit = try repository.mockCommit() + + // Create some new branches + let newBranchNames = ["develop", "feature", "hotfix"] + for name in newBranchNames { + try repository.branch.create(named: name, target: commit) } + + // Iterate using for-in (uses makeIterator) + var branches: [Branch] = [] + for branch in repository.branch { + branches.append(branch) + } + + // Check the count (including main) + #expect(branches.count == 4) + + // Verify all branches are present + let branchNames = branches.map(\.name).sorted() + let expectedNames = ["develop", "feature", "hotfix", "main"] + #expect(branchNames == expectedNames) } +} - func testBranchUpstream() async throws { - // Create a mock repository at the temporary directory +// MARK: - Remote Branch Operations + +@Suite("Branch Remote Operations", .tags(.branch, .collection, .remote)) +final class BranchRemoteTests: SwiftGitXTest { + @Test("Get upstream branch") + func branchGetUpstream() async throws { let source = URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! - let directory = Repository.mockDirectory(named: "test-branch-upstream", in: Self.directory) + let directory = mockDirectory() let repository = try await Repository.clone(from: source, to: directory) - // Get the upstream branch of the current branch - let upstreamBranch = try XCTUnwrap(repository.branch.current.upstream as? Branch) + let upstreamBranch = try #require(repository.branch.current.upstream as? Branch) - // Check the upstream branch - XCTAssertEqual(upstreamBranch.name, "origin/main") - XCTAssertEqual(upstreamBranch.fullName, "refs/remotes/origin/main") - XCTAssertEqual(upstreamBranch.type, .remote) + #expect(upstreamBranch.name == "origin/main") + #expect(upstreamBranch.fullName == "refs/remotes/origin/main") + #expect(upstreamBranch.type == .remote) } - func testBranchSetUpstream() async throws { - // Create a mock repository at the temporary directory + @Test("Set upstream branch") + func branchSetUpstream() async throws { let source = URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! - let directory = Repository.mockDirectory(named: "test-branch-set-upstream", in: Self.directory) + let directory = mockDirectory() let repository = try await Repository.clone(from: source, to: directory) // Unset the existing upstream branch try repository.branch.setUpstream(to: nil) + // Be sure that the upstream branch is unset - try XCTAssertNil(repository.branch.current.upstream) + #expect(try repository.branch.current.upstream == nil) // Set the upstream branch try repository.branch.setUpstream(to: repository.branch.get(named: "origin/main")) // Check if the upstream branch is set - let upstreamBranch = try XCTUnwrap(repository.branch.current.upstream as? Branch) - XCTAssertEqual(upstreamBranch.name, "origin/main") - XCTAssertEqual(upstreamBranch.fullName, "refs/remotes/origin/main") + let upstreamBranch = try #require(repository.branch.current.upstream as? Branch) + #expect(upstreamBranch.name == "origin/main") + #expect(upstreamBranch.fullName == "refs/remotes/origin/main") } - func testBranchUnsetUpstream() async throws { - // Create a mock repository at the temporary directory + @Test("Unset upstream branch") + func branchUnsetUpstream() async throws { let source = URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! - let directory = Repository.mockDirectory(named: "test-branch-unset-upstream", in: Self.directory) + let directory = mockDirectory() let repository = try await Repository.clone(from: source, to: directory) // Be sure that the upstream branch is set - try XCTAssertNotNil(repository.branch.current.upstream) + #expect(try repository.branch.current.upstream != nil) // Unset the upstream branch try repository.branch.setUpstream(to: nil) // Check if the upstream branch is unset - try XCTAssertNil(repository.branch.current.upstream) + #expect(try repository.branch.current.upstream == nil) + } + + @Test("Set upstream with explicit local branch") + func branchSetUpstreamExplicit() async throws { + let source = URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! + let directory = mockDirectory() + let repository = try await Repository.clone(from: source, to: directory) + + // Create a new local branch from the current HEAD + let commit = try #require(repository.HEAD.target as? Commit) + let newBranch = try repository.branch.create(named: "feature", target: commit) + + // Set upstream for the new branch explicitly + let upstreamBranch = try repository.branch.get(named: "origin/main") + try repository.branch.setUpstream(from: newBranch, to: upstreamBranch) + + // Check if the upstream branch is set correctly + let featureBranch = try repository.branch.get(named: "feature") + let upstream = try #require(featureBranch.upstream as? Branch) + #expect(upstream.name == "origin/main") + } + + @Test("Iterate remote branches") + func branchSequenceRemote() async throws { + let source = URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! + let directory = mockDirectory() + let repository = try await Repository.clone(from: source, to: directory) + + // Get the remote branches + let remoteBranches = Array(repository.branch.remote) + + // Should have at least one remote branch (origin/main) + #expect(!remoteBranches.isEmpty) + + // All branches should be remote type + for branch in remoteBranches { + #expect(branch.type == .remote) + #expect(branch.name.hasPrefix("origin/")) + } + } + + @Test("List remote branches") + func branchListRemote() async throws { + let source = URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! + let directory = mockDirectory() + let repository = try await Repository.clone(from: source, to: directory) + + // Get the remote branches + let remoteBranches = Array(repository.branch.remote) + + // Should have at least one remote branch (origin/main) + #expect(!remoteBranches.isEmpty) + + // All branches should be remote type + for branch in remoteBranches { + #expect(branch.type == .remote) + #expect(branch.name.hasPrefix("origin/")) + } + + // Verify we can lookup the remote branch + let originMain = try repository.branch.get(named: "origin/main", type: .remote) + #expect(remoteBranches.contains(originMain)) + } +} + +// MARK: - Error Cases + +@Suite("Branch Collection Error Cases", .tags(.branch, .collection, .error)) +final class BranchCollectionErrorTests: SwiftGitXTest { + @Test("Get non-existent branch throws error") + func branchGetNonExistent() async throws { + let repository = mockRepository() + try repository.mockCommit() + + #expect(throws: SwiftGitXError.self) { + try repository.branch.get(named: "non-existent-branch") + } + + // Subscript should return nil + #expect(repository.branch["non-existent-branch"] == nil) + } + + @Test("Get current branch in detached HEAD state throws error") + func branchCurrentDetachedHead() async throws { + let repository = mockRepository() + let commit = try repository.mockCommit() + + // Switch to a commit directly (creates detached HEAD) + try repository.switch(to: commit) + + // Getting current branch should throw + #expect(throws: SwiftGitXError.self) { + _ = try repository.branch.current + } + } + + @Test("Create branch from remote branch fails") + func branchCreateFromRemote() async throws { + let source = URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! + let directory = mockDirectory() + let repository = try await Repository.clone(from: source, to: directory) + + // Get a remote branch + let remoteBranch = try repository.branch.get(named: "origin/main", type: .remote) + + // Try to create a branch from remote (should fail) + #expect(throws: SwiftGitXError.self) { + try repository.branch.create(named: "new-branch", from: remoteBranch) + } + } + + @Test("Delete non-existent branch fails") + func branchDeleteNonExistent() async throws { + let repository = mockRepository() + try repository.mockCommit() + + // Create a branch and then lookup it to get a Branch object + let commit = try repository.mockCommit() + let branch = try repository.branch.create(named: "temp", target: commit) + + // Delete the branch + try repository.branch.delete(branch) + + // Try to delete again (should fail) + #expect(throws: SwiftGitXError.self) { + try repository.branch.delete(branch) + } + } + + @Test("Rename non-existent branch fails") + func branchRenameNonExistent() async throws { + let repository = mockRepository() + try repository.mockCommit() + + // Create a branch + let commit = try repository.mockCommit() + let branch = try repository.branch.create(named: "temp", target: commit) + + // Delete the branch + try repository.branch.delete(branch) + + // Try to rename the deleted branch (should fail) + #expect(throws: SwiftGitXError.self) { + try repository.branch.rename(branch, to: "new-name") + } } } diff --git a/Tests/SwiftGitXTests/CollectionTests/ConfigCollection.swift b/Tests/SwiftGitXTests/CollectionTests/ConfigCollection.swift deleted file mode 100644 index 60f370b..0000000 --- a/Tests/SwiftGitXTests/CollectionTests/ConfigCollection.swift +++ /dev/null @@ -1,41 +0,0 @@ -import SwiftGitX -import XCTest - -final class ConfigCollectionTests: SwiftGitXTestCase { - func testConfigDefaultBranchName() throws { - let repository = Repository.mock(named: "test-config-default-branch-name", in: Self.directory) - - // Set local default branch name - try repository.config.set("feature", forKey: "init.defaultBranch") - - XCTAssertEqual(try repository.config.defaultBranchName, "feature") - } - - func testConfigSet() throws { - let repository = Repository.mock(named: "test-config-set", in: Self.directory) - - // Set local default branch name - try repository.config.set("develop", forKey: "init.defaultBranch") - - // Test if the default branch name is set - XCTAssertEqual(try repository.config.defaultBranchName, "develop") - // Global default branch name should not be changed - XCTAssertEqual(try Repository.config.defaultBranchName, "main") - } - - func testConfigString() throws { - let repository = Repository.mock(named: "test-config-string", in: Self.directory) - - // Set local user name and email - try repository.config.set("İbrahim Çetin", forKey: "user.name") - try repository.config.set("mail@ibrahimcetin.dev", forKey: "user.email") - - XCTAssertEqual(try repository.config.string(forKey: "user.name"), "İbrahim Çetin") - XCTAssertEqual(try repository.config.string(forKey: "user.email"), "mail@ibrahimcetin.dev") - } - - func testConfigGlobalString() throws { - // Get global default branch name - XCTAssertEqual(try Repository.config.string(forKey: "init.defaultBranch"), "main") - } -} diff --git a/Tests/SwiftGitXTests/CollectionTests/ConfigCollectionTests.swift b/Tests/SwiftGitXTests/CollectionTests/ConfigCollectionTests.swift new file mode 100644 index 0000000..fdadbc7 --- /dev/null +++ b/Tests/SwiftGitXTests/CollectionTests/ConfigCollectionTests.swift @@ -0,0 +1,75 @@ +import Foundation +import SwiftGitX +import Testing + +@Suite("Config Collection", .tags(.config, .collection)) +final class ConfigCollectionTests: SwiftGitXTest { + @Test("Get and set default branch name") + func configDefaultBranchName() async throws { + let repository = mockRepository() + + // Set local default branch name + try repository.config.set("init.defaultBranch", to: "feature") + + #expect(try repository.config.defaultBranchName == "feature") + } + + @Test("Set config value locally without affecting global config") + func configSetLocal() async throws { + let repository = mockRepository() + + // Set local default branch name + try repository.config.set("init.defaultBranch", to: "develop") + + // Test if the default branch name is set + #expect(try repository.config.defaultBranchName == "develop") + // Global default branch name should not be changed + #expect(try Repository.config.defaultBranchName == "main") + } + + @Test("Get and set string config values") + func configString() async throws { + let repository = mockRepository() + + // Set local user name and email + try repository.config.set("user.name", to: "İbrahim Çetin") + try repository.config.set("user.email", to: "mail@ibrahimcetin.dev") + + #expect(try repository.config.string(forKey: "user.name") == "İbrahim Çetin") + #expect(try repository.config.string(forKey: "user.email") == "mail@ibrahimcetin.dev") + } + + @Test("Get global config value") + func configGlobalString() async throws { + // Get global default branch name + #expect(try Repository.config.string(forKey: "init.defaultBranch") == "main") + } + + @Test("Set and retrieve multiple config values") + func configMultipleValues() async throws { + let repository = mockRepository() + + // Set multiple configuration values + try repository.config.set("core.autocrlf", to: "true") + try repository.config.set("core.filemode", to: "false") + try repository.config.set("init.defaultBranch", to: "main") + + // Verify all values are set correctly + #expect(try repository.config.string(forKey: "core.autocrlf") == "true") + #expect(try repository.config.string(forKey: "core.filemode") == "false") + #expect(try repository.config.string(forKey: "init.defaultBranch") == "main") + } + + @Test("Override existing config value") + func configOverride() async throws { + let repository = mockRepository() + + // Set initial value + try repository.config.set("init.defaultBranch", to: "develop") + #expect(try repository.config.defaultBranchName == "develop") + + // Override with new value + try repository.config.set("init.defaultBranch", to: "main") + #expect(try repository.config.defaultBranchName == "main") + } +} diff --git a/Tests/SwiftGitXTests/CollectionTests/IndexCollectionTests.swift b/Tests/SwiftGitXTests/CollectionTests/IndexCollectionTests.swift index ef33cc6..509e8b6 100644 --- a/Tests/SwiftGitXTests/CollectionTests/IndexCollectionTests.swift +++ b/Tests/SwiftGitXTests/CollectionTests/IndexCollectionTests.swift @@ -1,187 +1,562 @@ -import XCTest +import Foundation +import Testing @testable import SwiftGitX -final class IndexCollectionTests: SwiftGitXTestCase { - func testIndexAddPath() throws { - // Create a repository - let repository = Repository.mock(named: "test-index-add-path", in: Self.directory) +// MARK: - Add Operations + +@Suite("Index Collection - Add Operations", .tags(.index, .collection)) +final class IndexAddOperationsTests: SwiftGitXTest { + @Test("Add file to index using path") + func indexAddPath() async throws { + let repository = mockRepository() // Create a file in the repository - _ = try repository.mockFile(named: "README.md", content: "Hello, World!") + let file = try repository.mockFile() // Stage the file using the file path - XCTAssertNoThrow(try repository.add(path: "README.md")) + try repository.add(file: file) // Verify that the file is staged - let statusEntry = try XCTUnwrap(repository.status().first) + let statusEntry = try #require(repository.status().first) - XCTAssertEqual(statusEntry.status, [.indexNew]) // The file is staged - XCTAssertEqual(statusEntry.index?.newFile.path, "README.md") - XCTAssertNil(statusEntry.workingTree) // The file is staged and not in the working tree anymore + #expect(statusEntry.status == [.indexNew]) // The file is staged + #expect(statusEntry.index?.newFile.path == "file-1.txt") + #expect(statusEntry.workingTree == nil) // The file is staged and not in the working tree anymore } - func testIndexAddFile() throws { - // Create a repository - let repository = Repository.mock(named: "test-index-add-file", in: Self.directory) + @Test("Add file to index using file URL") + func indexAddFile() async throws { + let repository = mockRepository() // Create a file in the repository - let file = try repository.mockFile(named: "README.md", content: "Hello, World!") + let file = try repository.mockFile() // Stage the file using the file URL - XCTAssertNoThrow(try repository.add(file: file)) + try repository.add(file: file) // Verify that the file is staged - let statusEntry = try XCTUnwrap(repository.status().first) + let statusEntry = try #require(repository.status().first) - XCTAssertEqual(statusEntry.status, [.indexNew]) // The file is staged - XCTAssertEqual(statusEntry.index?.newFile.path, "README.md") - XCTAssertNil(statusEntry.workingTree) // The file is staged and not in the working tree anymore + #expect(statusEntry.status == [.indexNew]) // The file is staged + #expect(statusEntry.index?.newFile.path == "file-1.txt") + #expect(statusEntry.workingTree == nil) // The file is staged and not in the working tree anymore } - func testIndexAddPaths() throws { - // Create a repository - let repository = Repository.mock(named: "test-index-add-paths", in: Self.directory) + @Test("Add multiple files to index using paths") + func indexAddPaths() async throws { + let repository = mockRepository() // Create new files in the repository - let files = try (0..<10).map { index in - try repository.mockFile(named: "README-\(index).md", content: "Hello, World!") + let files = try (0..<9).map { _ in + try repository.mockFile() } // Stage the files using the file paths - XCTAssertNoThrow(try repository.add(paths: files.map(\.lastPathComponent))) + try repository.add(paths: files.map(\.lastPathComponent)) // Verify that the files are staged let statusEntries = try repository.status() - XCTAssertEqual(statusEntries.count, files.count) - XCTAssertEqual(statusEntries.map(\.status), Array(repeating: [.indexNew], count: files.count)) - XCTAssertEqual(statusEntries.map(\.index?.newFile.path), files.map(\.lastPathComponent)) - XCTAssertEqual(statusEntries.map(\.workingTree), Array(repeating: nil, count: files.count)) + #expect(statusEntries.count == files.count) + #expect(statusEntries.map(\.status) == Array(repeating: [.indexNew], count: files.count)) + #expect(statusEntries.map(\.index?.newFile.path) == files.map(\.lastPathComponent)) + #expect(statusEntries.map(\.workingTree) == Array(repeating: nil, count: files.count)) } - func testIndexAddFiles() throws { - // Create a repository - let repository = Repository.mock(named: "test-index-add-files", in: Self.directory) + @Test("Add multiple files to index using file URLs") + func indexAddFiles() async throws { + let repository = mockRepository() // Create new files in the repository - let files = try (0..<10).map { index in - try repository.mockFile(named: "README-\(index).md", content: "Hello, World!") + let files = try (0..<9).map { _ in + try repository.mockFile() } // Stage the files using the file URLs - XCTAssertNoThrow(try repository.add(files: files)) + try repository.add(files: files) // Verify that the files are staged let statusEntries = try repository.status() - XCTAssertEqual(statusEntries.count, files.count) - XCTAssertEqual(statusEntries.map(\.status), Array(repeating: [.indexNew], count: files.count)) - XCTAssertEqual(statusEntries.map(\.index?.newFile.path), files.map(\.lastPathComponent)) - XCTAssertEqual(statusEntries.map(\.workingTree), Array(repeating: nil, count: files.count)) + #expect(statusEntries.count == files.count) + #expect(statusEntries.map(\.status) == Array(repeating: [.indexNew], count: files.count)) + #expect(statusEntries.map(\.index?.newFile.path) == files.map(\.lastPathComponent)) + #expect(statusEntries.map(\.workingTree) == Array(repeating: nil, count: files.count)) } +} - // TODO: Add test for add all +// MARK: - Remove Operations - func testIndexRemovePath() throws { - // Create a repository - let repository = Repository.mock(named: "test-index-remove-path", in: Self.directory) +@Suite("Index Collection - Remove Operations", .tags(.index, .collection)) +final class IndexRemoveOperationsTests: SwiftGitXTest { + @Test("Remove file from index using path") + func indexRemovePath() async throws { + let repository = mockRepository() // Create a file in the repository - let file = try repository.mockFile(named: "README.md", content: "Hello, World!") + let file = try repository.mockFile() // Stage the file - XCTAssertNoThrow(try repository.add(file: file)) + try repository.add(file: file) // Unstage the file using the file path - XCTAssertNoThrow(try repository.remove(path: "README.md")) + try repository.remove(path: "file-1.txt") // Verify that the file is not staged - let statusEntry = try XCTUnwrap(repository.status().first) + let statusEntry = try #require(repository.status().first) - XCTAssertEqual(statusEntry.status, [.workingTreeNew]) - XCTAssertNil(statusEntry.index) // The file is not staged + #expect(statusEntry.status == [.workingTreeNew]) + #expect(statusEntry.index == nil) // The file is not staged } - func testIndexRemoveFile() throws { - // Create a repository - let repository = Repository.mock(named: "test-index-remove-file", in: Self.directory) + @Test("Remove file from index using file URL") + func indexRemoveFile() async throws { + let repository = mockRepository() // Create a file in the repository - let file = try repository.mockFile(named: "README.md", content: "Hello, World!") + let file = try repository.mockFile() // Stage the file - XCTAssertNoThrow(try repository.add(file: file)) + try repository.add(file: file) // Unstage the file using the file URL - XCTAssertNoThrow(try repository.remove(file: file)) + try repository.remove(file: file) // Verify that the file is not staged - let statusEntry = try XCTUnwrap(repository.status().first) + let statusEntry = try #require(repository.status().first) - XCTAssertEqual(statusEntry.status, [.workingTreeNew]) - XCTAssertNil(statusEntry.index) // The file is not staged + #expect(statusEntry.status == [.workingTreeNew]) + #expect(statusEntry.index == nil) // The file is not staged } - func testIndexRemovePaths() throws { - // Create a repository - let repository = Repository.mock(named: "test-index-remove-paths", in: Self.directory) + @Test("Remove multiple files from index using paths") + func indexRemovePaths() async throws { + let repository = mockRepository() // Create new files in the repository - let files = try (0..<10).map { index in - try repository.mockFile(named: "README-\(index).md", content: "Hello, World!") + let files = try (0..<9).map { _ in + try repository.mockFile() } // Stage the files - XCTAssertNoThrow(try repository.add(files: files)) + try repository.add(files: files) // Unstage the files using the file paths - XCTAssertNoThrow(try repository.remove(paths: files.map(\.lastPathComponent))) + try repository.remove(paths: files.map(\.lastPathComponent)) // Verify that the files are not staged let statusEntries = try repository.status() - XCTAssertEqual(statusEntries.count, files.count) - XCTAssertEqual(statusEntries.map(\.status), Array(repeating: [.workingTreeNew], count: files.count)) - XCTAssertEqual(statusEntries.map(\.index), Array(repeating: nil, count: files.count)) + #expect(statusEntries.count == files.count) + #expect(statusEntries.map(\.status) == Array(repeating: [.workingTreeNew], count: files.count)) + #expect(statusEntries.map(\.index) == Array(repeating: nil, count: files.count)) } - func testIndexRemoveFiles() throws { - // Create a repository - let repository = Repository.mock(named: "test-index-remove-files", in: Self.directory) + @Test("Remove multiple files from index using file URLs") + func indexRemoveFiles() async throws { + let repository = mockRepository() // Create new files in the repository - let files = try (0..<10).map { index in - try repository.mockFile(named: "README-\(index).md", content: "Hello, World!") + let files = try (0..<9).map { _ in + try repository.mockFile() } // Stage the files - XCTAssertNoThrow(try repository.add(files: files)) + try repository.add(files: files) // Unstage the files using the file URLs - XCTAssertNoThrow(try repository.remove(files: files)) + try repository.remove(files: files) // Verify that the files are not staged let statusEntries = try repository.status() - XCTAssertEqual(statusEntries.count, files.count) - XCTAssertEqual(statusEntries.map(\.status), Array(repeating: [.workingTreeNew], count: files.count)) - XCTAssertEqual(statusEntries.map(\.index), Array(repeating: nil, count: files.count)) + #expect(statusEntries.count == files.count) + #expect(statusEntries.map(\.status) == Array(repeating: [.workingTreeNew], count: files.count)) + #expect(statusEntries.map(\.index) == Array(repeating: nil, count: files.count)) } - func testIndexRemoveAll() throws { - // Create a repository - let repository = Repository.mock(named: "test-index-remove-all", in: Self.directory) + @Test("Remove all files from index") + func indexRemoveAll() async throws { + let repository = mockRepository() // Create new files in the repository - let files = try (0..<10).map { index in - try repository.mockFile(named: "README-\(index).md", content: "Hello, World!") + let files = try (0..<9).map { _ in + try repository.mockFile() } // Stage the files - XCTAssertNoThrow(try repository.add(files: files)) + try repository.add(files: files) // Unstage all files - XCTAssertNoThrow(try repository.index.removeAll()) + try repository.index.removeAll() + + // Verify all files are unstaged + let statusEntries = try repository.status() + #expect(statusEntries.allSatisfy { $0.index == nil }) + } +} + +// MARK: - Subdirectory Operations + +@Suite("Index Collection - Subdirectories", .tags(.index, .collection)) +final class IndexSubdirectoryTests: SwiftGitXTest { + @Test("Add file in subdirectory using path") + func indexAddSubdirectoryPath() async throws { + let repository = mockRepository() + + // Create a subdirectory and file + let subdirPath = try repository.workingDirectory.appending(component: "src") + try FileManager.default.createDirectory(at: subdirPath, withIntermediateDirectories: true) + + let filePath = subdirPath.appending(component: "main.swift") + try "print(\"Hello\")".write(to: filePath, atomically: true, encoding: .utf8) + + // Stage the file using relative path + try repository.add(path: "src/main.swift") + + // Verify that the file is staged + let statusEntry = try #require(repository.status().first) + #expect(statusEntry.status == [.indexNew]) + #expect(statusEntry.index?.newFile.path == "src/main.swift") + } + + @Test("Add file in nested subdirectories") + func indexAddNestedSubdirectories() async throws { + let repository = mockRepository() + + // Create nested subdirectories + let nestedPath = try repository.workingDirectory.appending(components: "docs", "api", "v1") + try FileManager.default.createDirectory(at: nestedPath, withIntermediateDirectories: true) + + let filePath = nestedPath.appending(component: "endpoints.md") + try "# API Endpoints".write(to: filePath, atomically: true, encoding: .utf8) + + // Stage the file using file URL + try repository.add(file: filePath) + + // Verify that the file is staged + let statusEntry = try #require(repository.status().first) + #expect(statusEntry.status == [.indexNew]) + #expect(statusEntry.index?.newFile.path == "docs/api/v1/endpoints.md") + } + + @Test("Add multiple files in different subdirectories") + func indexAddMultipleSubdirectories() async throws { + let repository = mockRepository() + + // Create files in different subdirectories + var files: [URL] = [] + + for dir in ["src", "tests", "docs"] { + let dirPath = try repository.workingDirectory.appending(component: dir) + try FileManager.default.createDirectory(at: dirPath, withIntermediateDirectories: true) + + let filePath = dirPath.appending(component: "file.txt") + try "Content".write(to: filePath, atomically: true, encoding: .utf8) + files.append(filePath) + } + + // Stage all files + try repository.add(files: files) + + // Verify all files are staged + let statusEntries = try repository.status() + #expect(statusEntries.count == 3) + #expect(statusEntries.allSatisfy { $0.status == [.indexNew] }) + + let paths = statusEntries.compactMap(\.index?.newFile.path).sorted() + #expect(paths == ["docs/file.txt", "src/file.txt", "tests/file.txt"]) + } +} + +// MARK: - Modified Files Workflow + +@Suite("Index Collection - Modified Files", .tags(.index, .collection)) +final class IndexModifiedFilesTests: SwiftGitXTest { + @Test("Stage file then modify it shows both staged and modified") + func indexStageAndModify() async throws { + let repository = mockRepository() + + // Create and stage a file + let file = try repository.mockFile() + try repository.add(file: file) + + // Commit to make it tracked + try repository.commit(message: "Initial commit") + + // Modify the file + try "Modified content".write(to: file, atomically: true, encoding: .utf8) + + // Verify file shows as modified in working tree + let statusEntry = try #require(repository.status().first) + #expect(statusEntry.status == [.workingTreeModified]) + #expect(statusEntry.workingTree != nil) + } + + @Test("Restage modified file") + func indexRestageModifiedFile() async throws { + let repository = mockRepository() + + // Create, stage, and commit a file + let file = try repository.mockFile() + try repository.add(file: file) + try repository.commit(message: "Initial commit") + + // Modify and restage the file + try "Modified content".write(to: file, atomically: true, encoding: .utf8) + try repository.add(file: file) + + // Verify file is staged with new content + let statusEntry = try #require(repository.status().first) + #expect(statusEntry.status == [.indexModified]) + #expect(statusEntry.index != nil) + #expect(statusEntry.workingTree == nil) + } + + @Test("Stage file, modify it, stage again") + func indexMultipleStages() async throws { + let repository = mockRepository() + + // Create, stage, and commit initial version + let file = try repository.mockFile() + try repository.add(file: file) + try repository.commit(message: "Initial commit") + + // Modify and stage (version 2) + try "Modified version 2".write(to: file, atomically: true, encoding: .utf8) + try repository.add(file: file) + + // Modify again (version 3) - should show staged and modified + try "Modified version 3".write(to: file, atomically: true, encoding: .utf8) + + let statusEntry = try #require(repository.status().first) + #expect(statusEntry.status == [.indexModified, .workingTreeModified]) + } +} + +// MARK: - Error Cases + +@Suite("Index Collection - Error Cases", .tags(.index, .collection, .error)) +final class IndexErrorTests: SwiftGitXTest { + @Test("Add non-existent file throws error") + func indexAddNonExistentFile() async throws { + let repository = mockRepository() + + // Try to add a file that doesn't exist + #expect(throws: SwiftGitXError.self) { + try repository.add(path: "non-existent-file.txt") + } + } + + @Test("Remove file not in index succeeds as no-op") + func indexRemoveNotStaged() async throws { + let repository = mockRepository() + + // Create a file but don't stage it + let file = try repository.mockFile() + + // Verify file is not staged initially + let statusBefore = try repository.status().first + #expect(statusBefore?.status == [.workingTreeNew]) + #expect(statusBefore?.index == nil) + + // Try to remove it from index (should succeed as no-op since it's not in the index) + try repository.remove(path: file.lastPathComponent) + + // Verify file is still not staged (nothing changed) + let statusAfter = try repository.status().first + #expect(statusAfter?.status == [.workingTreeNew]) + #expect(statusAfter?.index == nil) + } + + @Test("Add file outside repository throws error") + func indexAddFileOutsideRepo() async throws { + let repository = mockRepository() + + // Create a file outside the repository + let tempFile = FileManager.default.temporaryDirectory.appending(component: "outside.txt") + try "content".write(to: tempFile, atomically: true, encoding: .utf8) + defer { try? FileManager.default.removeItem(at: tempFile) } + + // Try to add it (should fail) + #expect(throws: SwiftGitXError.self) { + try repository.add(file: tempFile) + } + } + + @Test("Add file with invalid path throws error") + func indexAddInvalidPath() async throws { + let repository = mockRepository() + + // Try to add with invalid/empty path + #expect(throws: SwiftGitXError.self) { + try repository.add(path: "") + } + } +} + +// MARK: - Edge Cases + +@Suite("Index Collection - Edge Cases", .tags(.index, .collection)) +final class IndexEdgeCasesTests: SwiftGitXTest { + @Test("Add empty array of files succeeds") + func indexAddEmptyArray() async throws { + let repository = mockRepository() + + // Add empty array (should succeed but do nothing) + try repository.add(files: []) + try repository.add(paths: []) + + // Verify index is still empty + let statusEntries = try repository.status() + #expect(statusEntries.isEmpty) + } + + @Test("Remove with empty array removes all files from index") + func indexRemoveEmptyArray() async throws { + let repository = mockRepository() + + // Create and stage multiple files + let file1 = try repository.mockFile() + let file2 = try repository.mockFile() + let file3 = try repository.mockFile() + try repository.add(files: [file1, file2, file3]) + + // Verify files are staged + let statusBefore = try repository.status() + #expect(statusBefore.count == 3) + #expect(statusBefore.allSatisfy { $0.status == [.indexNew] }) + + // Remove with empty array (empty pathspec matches all files) + try repository.remove(paths: []) + + // Verify all files are unstaged (removed from index) + let statusAfter = try repository.status() + #expect(statusAfter.count == 3) + #expect(statusAfter.allSatisfy { $0.status == [.workingTreeNew] }) + #expect(statusAfter.allSatisfy { $0.index == nil }) + } + + @Test("Add file with spaces in name") + func indexAddFileWithSpaces() async throws { + let repository = mockRepository() + + // Create file with spaces in name + let file = try repository.mockFile() + try repository.add(file: file) + + // Verify file is staged + let statusEntry = try #require(repository.status().first) + #expect(statusEntry.status == [.indexNew]) + #expect(statusEntry.index?.newFile.path == "file-1.txt") + } + + @Test("Add file with special characters in name") + func indexAddFileWithSpecialCharacters() async throws { + let repository = mockRepository() + + // Create file with special characters (that are valid in filenames) + let file = try repository.mockFile() + try repository.add(file: file) + + // Verify file is staged + let statusEntry = try #require(repository.status().first) + #expect(statusEntry.status == [.indexNew]) + #expect(statusEntry.index?.newFile.path == "file-1.txt") + } +} + +// MARK: - Mixed Operations + +@Suite("Index Collection - Mixed Operations", .tags(.index, .collection)) +final class IndexMixedOperationsTests: SwiftGitXTest { + @Test("Add and remove different files in one operation") + func indexMixedAddRemove() async throws { + let repository = mockRepository() + + // Create and stage initial files + let file1 = try repository.mockFile() + let file2 = try repository.mockFile() + try repository.add(files: [file1, file2]) + try repository.commit(message: "Initial commit") + + // Create new files to add + let file3 = try repository.mockFile() + + // Add new file and remove one old file + try repository.add(file: file3) + try FileManager.default.removeItem(at: file1) + try repository.remove(file: file1) // Stage the deletion + + // Verify mixed state + let statusEntries = try repository.status() + #expect(statusEntries.count == 2) + + // file1 should be staged for deletion + let file1Status = statusEntries.first(where: { $0.index?.newFile.path == "file-1.txt" }) + #expect(file1Status?.status == [.indexDeleted]) + + // file3 should be new in index + let file3Status = statusEntries.first(where: { $0.index?.newFile.path == "file-3.txt" }) + #expect(file3Status?.status == [.indexNew]) + } + + @Test("Stage files in multiple steps and verify cumulative state") + func indexCumulativeStaging() async throws { + let repository = mockRepository() + + // Create files + let file1 = try repository.mockFile() + let file2 = try repository.mockFile() + let file3 = try repository.mockFile() + + // Stage files one by one + try repository.add(file: file1) + var statusEntries = try repository.status().filter { $0.status == [.indexNew] } + #expect(statusEntries.count == 1) + + try repository.add(file: file2) + statusEntries = try repository.status().filter { $0.status == [.indexNew] } + #expect(statusEntries.count == 2) + + try repository.add(file: file3) + statusEntries = try repository.status().filter { $0.status == [.indexNew] } + #expect(statusEntries.count == 3) + + // Verify all are staged + #expect(statusEntries.allSatisfy { $0.status == [.indexNew] }) + + let paths = statusEntries.compactMap(\.index?.newFile.path).sorted() + let expectedPaths = [file1, file2, file3].map(\.lastPathComponent).sorted() + #expect(paths == expectedPaths) + } + + @Test("Partial unstaging of files") + func indexPartialUnstaging() async throws { + let repository = mockRepository() + + // Create and stage multiple files + let files = try (0..<5).map { _ in + try repository.mockFile() + } + try repository.add(files: files) + + // Verify all are staged + var statusEntries = try repository.status() + #expect(statusEntries.count == 5) + + // Unstage only 2 files + try repository.remove(files: [files[1], files[3]]) + + // Verify partial unstaging + statusEntries = try repository.status() + #expect(statusEntries.count == 5) + + let stagedFiles = statusEntries.filter { $0.status == [.indexNew] } + let unstagedFiles = statusEntries.filter { $0.status == [.workingTreeNew] } + + #expect(stagedFiles.count == 3) + #expect(unstagedFiles.count == 2) } } diff --git a/Tests/SwiftGitXTests/CollectionTests/ReferenceCollectionTests.swift b/Tests/SwiftGitXTests/CollectionTests/ReferenceCollectionTests.swift index d071d80..dc7dcb9 100644 --- a/Tests/SwiftGitXTests/CollectionTests/ReferenceCollectionTests.swift +++ b/Tests/SwiftGitXTests/CollectionTests/ReferenceCollectionTests.swift @@ -1,29 +1,28 @@ +import Foundation import SwiftGitX -import XCTest +import Testing -final class ReferenceCollectionTests: SwiftGitXTestCase { - func testReferenceLookupSubscript() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-reference-lookup-subscript", in: Self.directory) +@Suite("Reference Collection", .tags(.reference, .collection)) +final class ReferenceCollectionTests: SwiftGitXTest { + @Test("Lookup reference by name using subscript") + func referenceLookupSubscript() async throws { + let repository = mockRepository() // Create mock commit let commit = try repository.mockCommit() // Get the branch - guard let reference = repository.reference["refs/heads/main"] else { - XCTFail("Reference not found") - return - } + let reference = try #require(repository.reference["refs/heads/main"]) // Check the reference - XCTAssertEqual(reference.name, "main") - XCTAssertEqual(reference.fullName, "refs/heads/main") - XCTAssertEqual(reference.target.id, commit.id) + #expect(reference.name == "main") + #expect(reference.fullName == "refs/heads/main") + #expect(reference.target.id == commit.id) } - func testReferenceLookupSubscriptFailure() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-reference-lookup-subscript-failure", in: Self.directory) + @Test("Lookup non-existent reference using subscript returns nil") + func referenceLookupSubscriptFailure() async throws { + let repository = mockRepository() // Create mock commit try repository.mockCommit() @@ -32,12 +31,12 @@ final class ReferenceCollectionTests: SwiftGitXTestCase { let reference = repository.reference["refs/heads/feature"] // Check the reference - XCTAssertNil(reference) + #expect(reference == nil) } - func testReferenceLookupBranch() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-reference-lookup-branch", in: Self.directory) + @Test("Lookup branch reference") + func referenceLookupBranch() async throws { + let repository = mockRepository() // Create mock commit let commit = try repository.mockCommit() @@ -49,14 +48,14 @@ final class ReferenceCollectionTests: SwiftGitXTestCase { let reference = try repository.reference.get(named: branch.fullName) // Check the reference - XCTAssertEqual(reference.name, branch.name) - XCTAssertEqual(reference.fullName, branch.fullName) - XCTAssertEqual(reference.target.id, commit.id) + #expect(reference.name == branch.name) + #expect(reference.fullName == branch.fullName) + #expect(reference.target.id == commit.id) } - func testReferenceLookupTagAnnotated() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-reference-lookup-tag-annotated", in: Self.directory) + @Test("Lookup annotated tag reference") + func referenceLookupTagAnnotated() async throws { + let repository = mockRepository() // Create mock commit let commit = try repository.mockCommit() @@ -68,14 +67,14 @@ final class ReferenceCollectionTests: SwiftGitXTestCase { let reference = try repository.reference.get(named: tag.fullName) // Check the reference - XCTAssertEqual(reference.name, "v1.0.0") - XCTAssertEqual(reference.fullName, "refs/tags/v1.0.0") - XCTAssertEqual(reference.target.id, commit.id) + #expect(reference.name == "v1.0.0") + #expect(reference.fullName == "refs/tags/v1.0.0") + #expect(reference.target.id == commit.id) } - func testReferenceLookupTagLightweight() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-reference-lookup-tag-lightweight", in: Self.directory) + @Test("Lookup lightweight tag reference") + func referenceLookupTagLightweight() async throws { + let repository = mockRepository() // Create mock commit let commit = try repository.mockCommit() @@ -87,32 +86,27 @@ final class ReferenceCollectionTests: SwiftGitXTestCase { let reference = try repository.reference.get(named: tag.fullName) // Check the reference - XCTAssertEqual(reference.name, "v1.0.0") - XCTAssertEqual(reference.fullName, "refs/tags/v1.0.0") - XCTAssertEqual(reference.target.id, commit.id) + #expect(reference.name == "v1.0.0") + #expect(reference.fullName == "refs/tags/v1.0.0") + #expect(reference.target.id == commit.id) } - func testReferenceLookupFailure() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-reference-lookup-failure", in: Self.directory) + @Test("Lookup non-existent reference throws error") + func referenceLookupFailure() async throws { + let repository = mockRepository() // Create mock commit try repository.mockCommit() - // Get the branch - XCTAssertThrowsError(try repository.reference.get(named: "refs/heads/feature")) { error in - XCTAssertTrue(error is SwiftGitXError) - let error = error as? SwiftGitXError - - XCTAssertEqual(error?.code, .notFound) - XCTAssertEqual(error?.category, .reference) - XCTAssertEqual(error?.message, "reference \'refs/heads/feature\' not found") + // Get the branch and verify error details + #expect(throws: SwiftGitXError.self) { + try repository.reference.get(named: "refs/heads/feature") } } - func testReferenceList() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-reference-list", in: Self.directory) + @Test("List all references") + func referenceList() async throws { + let repository = mockRepository() // Create mock commit let commit = try repository.mockCommit() @@ -127,17 +121,17 @@ final class ReferenceCollectionTests: SwiftGitXTestCase { let references = try repository.reference.list() // Check the reference - XCTAssertEqual(references.count, 3) + #expect(references.count == 3) let referenceNames = references.map(\.name) - XCTAssertTrue(referenceNames.contains("feature")) - XCTAssertTrue(referenceNames.contains("main")) - XCTAssertTrue(referenceNames.contains("v1.0.0")) + #expect(referenceNames.contains("feature")) + #expect(referenceNames.contains("main")) + #expect(referenceNames.contains("v1.0.0")) } - func testReferenceIterator() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-reference-iterator", in: Self.directory) + @Test("Iterate over all references") + func referenceIterator() async throws { + let repository = mockRepository() // Create mock commit let commit = try repository.mockCommit() @@ -152,17 +146,17 @@ final class ReferenceCollectionTests: SwiftGitXTestCase { let references = Array(repository.reference) // Check the reference - XCTAssertEqual(references.count, 3) + #expect(references.count == 3) let referenceNames = references.map(\.name) - XCTAssertTrue(referenceNames.contains("feature")) - XCTAssertTrue(referenceNames.contains("main")) - XCTAssertTrue(referenceNames.contains("v1.0.0")) + #expect(referenceNames.contains("feature")) + #expect(referenceNames.contains("main")) + #expect(referenceNames.contains("v1.0.0")) } - func testReferenceIteratorGlob() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-reference-iterator-glob", in: Self.directory) + @Test("List references with glob pattern") + func referenceIteratorGlob() async throws { + let repository = mockRepository() // Create mock commit let commit = try repository.mockCommit() @@ -174,9 +168,63 @@ final class ReferenceCollectionTests: SwiftGitXTestCase { let references = try repository.reference.list(glob: "refs/tags/*") // Check the references - XCTAssertEqual(references.count, 1) - let tagLookup = try XCTUnwrap(references.first as? Tag) + #expect(references.count == 1) + + let tagLookup = try #require(references.first as? SwiftGitX.Tag) + + #expect(tagLookup == tag) + } + + @Test("List only branches with glob pattern") + func referenceListBranchesOnly() async throws { + let repository = mockRepository() + + // Create mock commit + let commit = try repository.mockCommit() + + // Create branches and tags + try repository.branch.create(named: "feature", target: commit) + try repository.branch.create(named: "develop", target: commit) + try repository.tag.create(named: "v1.0.0", target: commit) + + // Get only branches using glob + let branches = try repository.reference.list(glob: "refs/heads/*") - XCTAssertEqual(tagLookup, tag) + // Should only return branches (main, feature, develop), not the tag + #expect(branches.count == 3) + + let branchNames = branches.map(\.name).sorted() + #expect(branchNames == ["develop", "feature", "main"]) + } + + @Test("List with glob pattern that matches nothing returns empty array") + func referenceListGlobNoMatches() async throws { + let repository = mockRepository() + + // Create mock commit + try repository.mockCommit() + + // Get references with a glob that matches nothing + let references = try repository.reference.list(glob: "refs/nonexistent/*") + + // Should return empty array + #expect(references.isEmpty) + } + + @Test("Lookup reference with invalid name throws error") + func referenceGetInvalidName() async throws { + let repository = mockRepository() + + // Create mock commit + try repository.mockCommit() + + // Try to get reference with invalid names + #expect(throws: SwiftGitXError.self) { + try repository.reference.get(named: "") + } + + #expect(throws: SwiftGitXError.self) { + try repository.reference.get(named: "main") // Missing refs/heads/ prefix + } } } diff --git a/Tests/SwiftGitXTests/CollectionTests/RemoteCollectionTests.swift b/Tests/SwiftGitXTests/CollectionTests/RemoteCollectionTests.swift index 97b09cc..8086a88 100644 --- a/Tests/SwiftGitXTests/CollectionTests/RemoteCollectionTests.swift +++ b/Tests/SwiftGitXTests/CollectionTests/RemoteCollectionTests.swift @@ -1,10 +1,12 @@ +import Foundation import SwiftGitX -import XCTest +import Testing -final class RemoteCollectionTests: SwiftGitXTestCase { - func testRemoteLookup() throws { - // Create a mock repository at the temporary directory - let repository = Repository.mock(named: "test-remote-lookup", in: Self.directory) +@Suite("Remote Collection", .tags(.remote, .collection)) +final class RemoteCollectionTests: SwiftGitXTest { + @Test("Lookup remote by name") + func remoteLookup() async throws { + let repository = mockRepository() // Add a remote to the repository let url = URL(string: "https://github.com/username/repo.git")! @@ -14,15 +16,14 @@ final class RemoteCollectionTests: SwiftGitXTestCase { let remoteLookup = try repository.remote.get(named: "origin") // Check if the remote is the same - XCTAssertEqual(remoteLookup, remote) - - XCTAssertEqual(remote.name, "origin") - XCTAssertEqual(remote.url, url) + #expect(remoteLookup == remote) + #expect(remote.name == "origin") + #expect(remote.url == url) } - func testRemoteAdd() throws { - // Create a mock repository at the temporary directory - let repository = Repository.mock(named: "test-remote-add", in: Self.directory) + @Test("Add remote to repository") + func remoteAdd() async throws { + let repository = mockRepository() // Add a new remote to the repository let url = URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! @@ -32,46 +33,44 @@ final class RemoteCollectionTests: SwiftGitXTestCase { let remoteLookup = try repository.remote.get(named: "origin") // Check if the remote is the same - XCTAssertEqual(remoteLookup, remote) - - XCTAssertEqual(remote.name, "origin") - XCTAssertEqual(remote.url, url) + #expect(remoteLookup == remote) + #expect(remote.name == "origin") + #expect(remote.url == url) } - func testRemoteBranches() async throws { - // Create a mock repository at the temporary directory - let remoteRepository = Repository.mock(named: "test-remote-branches--remote", in: Self.directory) + @Test("Get remote branches after clone") + func remoteBranches() async throws { + let remoteRepository = mockRepository() // Create a commit in the repository try remoteRepository.mockCommit() // Create branches in the repository - try ["feature/1", "feature/2", "feature/3", "feature/4", "feature/5", "feature/6", "feature/7"] - .forEach { name in - try remoteRepository.branch.create(named: name, from: remoteRepository.branch.current) - } + for name in ["feature/1", "feature/2", "feature/3", "feature/4", "feature/5", "feature/6", "feature/7"] { + try remoteRepository.branch.create(named: name, from: remoteRepository.branch.current) + } let branches = Array(remoteRepository.branch.local) - XCTAssertEqual(branches.count, 8) + #expect(branches.count == 8) // Clone remote repository to local repository - let localDirectory = Repository.mockDirectory(named: "test-remote-branches--local", in: Self.directory) + let localDirectory = mockDirectory(suffix: "--local") let localRepository = try await Repository.clone(from: remoteRepository.workingDirectory, to: localDirectory) // Get the remote from the repository excluding the main branch let remoteBranches = Array(localRepository.branch.remote) // Check if the branches are the same - XCTAssertEqual(remoteBranches.count, 8) + #expect(remoteBranches.count == 8) for (remoteBranch, branch) in zip(remoteBranches, branches) { - XCTAssertEqual(remoteBranch.name, "origin/" + branch.name) + #expect(remoteBranch.name == "origin/" + branch.name) } } - func testRemoteRemove() throws { - // Create a mock repository at the temporary directory - let repository = Repository.mock(named: "test-remote-remove", in: Self.directory) + @Test("Remove remote from repository") + func remoteRemove() async throws { + let repository = mockRepository() // Add a remote to the repository let remote = try repository.remote.add( @@ -82,21 +81,19 @@ final class RemoteCollectionTests: SwiftGitXTestCase { // Remove the remote from the repository try repository.remote.remove(remote) - // Get the remote from the repository - XCTAssertThrowsError(try repository.remote.get(named: "origin")) { error in - XCTAssertTrue(error is SwiftGitXError) - - let error = error as? SwiftGitXError - - XCTAssertEqual(error?.code, .notFound) - XCTAssertEqual(error?.category, .config) - XCTAssertEqual(error?.message, "remote \'origin\' does not exist") + // Get the remote from the repository (should throw) + let error = #expect(throws: SwiftGitXError.self) { + try repository.remote.get(named: "origin") } + + #expect(error?.code == .notFound) + #expect(error?.category == .config) + #expect(error?.message == "remote \'origin\' does not exist") } - func testRemoteList() throws { - // Create a mock repository at the temporary directory - let repository = Repository.mock(named: "test-remote-list", in: Self.directory) + @Test("List all remotes") + func remoteList() async throws { + let repository = mockRepository() // Add remotes to the repository let remoteNames = ["origin", "upstream", "features", "my-remote", "remote"] @@ -107,12 +104,22 @@ final class RemoteCollectionTests: SwiftGitXTestCase { // List the remotes in the repository let remoteLookups = try repository.remote.list() - XCTAssertEqual(Set(remotes), Set(remoteLookups)) + #expect(Set(remotes) == Set(remoteLookups)) + } + + @Test("List remotes on empty repository returns empty array") + func remoteListEmpty() async throws { + let repository = mockRepository() + + // List remotes (should be empty) + let remotes = try repository.remote.list() + + #expect(remotes.isEmpty) } - func testRemoteIterator() throws { - // Create a mock repository at the temporary directory - let repository = Repository.mock(named: "test-remote-iterator", in: Self.directory) + @Test("Iterate over all remotes") + func remoteIterator() async throws { + let repository = mockRepository() // Add remotes to the repository let remoteNames = ["origin", "upstream", "features", "my-remote", "remote"] @@ -123,28 +130,36 @@ final class RemoteCollectionTests: SwiftGitXTestCase { // List the remotes in the repository let remoteLookups = Array(repository.remote) - XCTAssertEqual(Set(remotes), Set(remoteLookups)) + #expect(Set(remotes) == Set(remoteLookups)) } - func testRemoteLookupNotFound() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-remote-not-found", in: Self.directory) + @Test("Iterate over empty repository returns no remotes") + func remoteIteratorEmpty() async throws { + let repository = mockRepository() + + // Iterate over remotes (should be empty) + let remotes = Array(repository.remote) - // Get the remote - XCTAssertThrowsError(try repository.remote.get(named: "origin")) { error in - XCTAssertTrue(error is SwiftGitXError) + #expect(remotes.isEmpty) + } - let error = error as? SwiftGitXError + @Test("Lookup non-existent remote throws error") + func remoteLookupNotFound() async throws { + let repository = mockRepository() - XCTAssertEqual(error?.code, .notFound) - XCTAssertEqual(error?.category, .config) - XCTAssertEqual(error?.message, "remote \'origin\' does not exist") + // Get the remote (should throw) + let error = #expect(throws: SwiftGitXError.self) { + try repository.remote.get(named: "origin") } + + #expect(error?.code == .notFound) + #expect(error?.category == .config) + #expect(error?.message == "remote \'origin\' does not exist") } - func testRemoteAddFailure() throws { - // Create a mock repository at the temporary directory - let repository = Repository.mock(named: "test-remote-remove", in: Self.directory) + @Test("Add duplicate remote throws error") + func remoteAddFailure() async throws { + let repository = mockRepository() // Add a remote to the repository let remote = try repository.remote.add( @@ -152,21 +167,19 @@ final class RemoteCollectionTests: SwiftGitXTestCase { at: URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! ) - // Add the same remote again - XCTAssertThrowsError(try repository.remote.add(named: "origin", at: remote.url)) { error in - XCTAssertTrue(error is SwiftGitXError) - - let error = error as? SwiftGitXError - - XCTAssertEqual(error?.code, .exists) - XCTAssertEqual(error?.category, .config) - XCTAssertEqual(error?.message, "remote \'origin\' already exists") + // Add the same remote again (should throw) + let error = #expect(throws: SwiftGitXError.self) { + try repository.remote.add(named: "origin", at: remote.url) } + + #expect(error?.code == .exists) + #expect(error?.category == .config) + #expect(error?.message == "remote \'origin\' already exists") } - func testRemoteRemoveFailure() throws { - // Create a mock repository at the temporary directory - let repository = Repository.mock(named: "test-remote-remove", in: Self.directory) + @Test("Remove non-existent remote throws error") + func remoteRemoveFailure() async throws { + let repository = mockRepository() // Add a remote to the repository let remote = try repository.remote.add( @@ -177,15 +190,41 @@ final class RemoteCollectionTests: SwiftGitXTestCase { // Remove the remote from the repository try repository.remote.remove(remote) - // Remove the remote again - XCTAssertThrowsError(try repository.remote.remove(remote)) { error in - XCTAssertTrue(error is SwiftGitXError) + // Remove the remote again (should throw) + let error = #expect(throws: SwiftGitXError.self) { + try repository.remote.remove(remote) + } - let error = error as? SwiftGitXError + #expect(error?.code == .notFound) + #expect(error?.category == .config) + #expect(error?.message == "remote \'origin\' does not exist") + } - XCTAssertEqual(error?.code, .notFound) - XCTAssertEqual(error?.category, .config) - XCTAssertEqual(error?.message, "remote \'origin\' does not exist") - } + @Test("Lookup remote using subscript") + func remoteSubscriptLookup() async throws { + let repository = mockRepository() + + // Add a remote to the repository + let url = URL(string: "https://github.com/username/repo.git")! + let remote = try repository.remote.add(named: "origin", at: url) + + // Get the remote using subscript + let remoteLookup = repository.remote["origin"] + + // Check if the remote is the same + #expect(remoteLookup == remote) + #expect(remoteLookup?.name == "origin") + #expect(remoteLookup?.url == url) + } + + @Test("Lookup non-existent remote using subscript returns nil") + func remoteSubscriptNotFound() async throws { + let repository = mockRepository() + + // Get the remote using subscript (should return nil) + let remote = repository.remote["nonexistent"] + + #expect(remote == nil) } + } diff --git a/Tests/SwiftGitXTests/CollectionTests/StashCollectionTests.swift b/Tests/SwiftGitXTests/CollectionTests/StashCollectionTests.swift index 61468ba..23c75db 100644 --- a/Tests/SwiftGitXTests/CollectionTests/StashCollectionTests.swift +++ b/Tests/SwiftGitXTests/CollectionTests/StashCollectionTests.swift @@ -1,172 +1,311 @@ +import Foundation import SwiftGitX -import XCTest +import Testing -final class StashCollectionTests: SwiftGitXTestCase { - func testStashSave() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-stash-save", in: Self.directory) +// MARK: - Save Operations - // Create mock commit +@Suite("Stash Collection - Save Operations", .tags(.stash, .collection)) +final class StashSaveTests: SwiftGitXTest { + @Test("Save staged changes to stash") + func saveStaged() async throws { + let repository = mockRepository() try repository.mockCommit() - // Create a file - let fileURL = try URL(fileURLWithPath: "test.txt", relativeTo: repository.workingDirectory) - FileManager.default.createFile(atPath: fileURL.path, contents: Data("Stash me!".utf8)) + // Create and stage a file + let file = try repository.mockFile() + try repository.add(file: file) - // Stage the file - try repository.add(path: fileURL.lastPathComponent) - - // Create a new stash entry + // Stash the changes try repository.stash.save() - // List the stash entries let stashes = try repository.stash.list() + #expect(stashes.count == 1) + } + + @Test("Save with custom message") + func saveWithMessage() async throws { + let repository = mockRepository() + try repository.mockCommit() + + // Create and stage a file + try repository.add(file: repository.mockFile()) + try repository.stash.save(message: "Work in progress") - // Check the stash entries - XCTAssertEqual(stashes.count, 1) + let stashes = try repository.stash.list() + #expect(stashes.count == 1) + #expect(stashes[0].message == "On main: Work in progress") } - func testStashSaveFailure() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-stash-save-failure", in: Self.directory) + @Test("Save with includeUntracked option") + func saveIncludeUntracked() async throws { + let repository = mockRepository() + try repository.mockCommit() + + // Create untracked file (not staged) + let file = try repository.mockFile() + + // Stash with includeUntracked + try repository.stash.save(options: .includeUntracked) + + // File should be removed from working directory + #expect(FileManager.default.fileExists(atPath: file.path) == false) + #expect(try repository.stash.list().count == 1) + } - // Create mock commit + @Test("Save multiple stashes") + func saveMultiple() async throws { + let repository = mockRepository() try repository.mockCommit() - // Create a new stash entry - XCTAssertThrowsError(try repository.stash.save()) { error in - let error = error as? SwiftGitXError + // Create multiple stashes + for i in 0..<3 { + _ = try repository.mockFile() + try repository.stash.save(message: "Stash #\(i)", options: .includeUntracked) + } - XCTAssertEqual(error?.code, .notFound) - XCTAssertEqual(error?.category, .stash) - XCTAssertEqual(error?.message, "cannot stash changes - there is nothing to stash.") + let stashes = try repository.stash.list() + #expect(stashes.count == 3) + + // Verify order (most recent first, LIFO) + #expect(stashes[0].message == "On main: Stash #2") + #expect(stashes[1].message == "On main: Stash #1") + #expect(stashes[2].message == "On main: Stash #0") + } + + @Test("Save with nothing to stash throws error", .tags(.error)) + func saveNothingThrows() async throws { + let repository = mockRepository() + try repository.mockCommit() + + let error = #expect(throws: SwiftGitXError.self) { + try repository.stash.save() } + + #expect(error?.code == .notFound) + #expect(error?.category == .stash) + #expect(error?.message == "cannot stash changes - there is nothing to stash.") } +} - func testStashList() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-stash-list", in: Self.directory) +// MARK: - List & Iterator Operations - // Create mock commit +@Suite("Stash Collection - List & Iterator", .tags(.stash, .collection)) +final class StashListTests: SwiftGitXTest { + @Test("List returns empty array when no stashes exist") + func listEmpty() async throws { + let repository = mockRepository() try repository.mockCommit() - for index in 0..<5 { - // Create a file - _ = try repository.mockFile(named: "test\(index).txt", content: "Stash me!") + let stashes = try repository.stash.list() + #expect(stashes.isEmpty) + } - // Create a new stash - try repository.stash.save(message: "Stashed \(index)!", options: .includeUntracked) + @Test("List returns all stash entries") + func listAll() async throws { + let repository = mockRepository() + try repository.mockCommit() + + // Create stashes + for i in 0..<3 { + try repository.add(file: repository.mockFile()) + try repository.stash.save(message: "Stash #\(i)") } - // List the stash entries let stashes = try repository.stash.list() - - // Check the stash entries - XCTAssertEqual(stashes.count, 5) + #expect(stashes.count == 3) } - func testStashIterator() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-stash-iterator", in: Self.directory) - - // Create mock commit + @Test("Iterate over stash entries with correct index") + func iterateWithIndex() async throws { + let repository = mockRepository() try repository.mockCommit() - for index in 0..<5 { - // Create a file - _ = try repository.mockFile(named: "test-\(index).txt", content: "Stash me!") - - // Create a new stash - try repository.stash.save(message: "Stashed \(index)!", options: .includeUntracked) + // Create stashes + for i in 0..<5 { + _ = try repository.mockFile() + try repository.stash.save(message: "Stash #\(i)", options: .includeUntracked) } - // Iterate over the stash entries + // Iterate and verify indices for (index, entry) in repository.stash.enumerated() { - XCTAssertEqual(entry.index, index) - XCTAssertEqual(entry.message, "On main: Stashed \(4 - index)!") + #expect(entry.index == index) + // Most recent is index 0 (Stash 4), oldest is index 4 (Stash 0) + #expect(entry.message == "On main: Stash #\(4 - index)") } } +} - func testStashApply() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-stash-apply", in: Self.directory) +// MARK: - Apply Operations - // Create mock commit +@Suite("Stash Collection - Apply Operations", .tags(.stash, .collection)) +final class StashApplyTests: SwiftGitXTest { + @Test("Apply restores changes and keeps stash entry") + func applyKeepsEntry() async throws { + let repository = mockRepository() try repository.mockCommit() - // Create a file - let fileURL = try URL(fileURLWithPath: "test.txt", relativeTo: repository.workingDirectory) - FileManager.default.createFile(atPath: fileURL.path, contents: Data("Stash me!".utf8)) - - // Create a new stash entry + // Create and stash a file + let file = try repository.mockFile() try repository.stash.save(options: .includeUntracked) - XCTAssertEqual(try repository.stash.list().count, 1) - XCTAssertFalse(FileManager.default.fileExists(atPath: fileURL.path)) + #expect(FileManager.default.fileExists(atPath: file.path) == false) - // Apply the stash entry + // Apply the stash try repository.stash.apply() - // List the stashes + // File restored, stash still exists + #expect(FileManager.default.fileExists(atPath: file.path) == true) + #expect(try repository.stash.list().count == 1) + } + + @Test("Apply specific stash entry by reference") + func applySpecific() async throws { + let repository = mockRepository() + try repository.mockCommit() + + // Create two stashes with different files + let file1 = try repository.mockFile(name: "first.txt") + try repository.stash.save(message: "First", options: .includeUntracked) + + let file2 = try repository.mockFile(name: "second.txt") + try repository.stash.save(message: "Second", options: .includeUntracked) + + // Both files gone + #expect(FileManager.default.fileExists(atPath: file1.path) == false) + #expect(FileManager.default.fileExists(atPath: file2.path) == false) + + // Apply the older stash (index 1) let stashes = try repository.stash.list() + try repository.stash.apply(stashes[1]) - // Check the stash entries - XCTAssertEqual(stashes.count, 1) // The stash should still exist - XCTAssertTrue(FileManager.default.fileExists(atPath: fileURL.path)) - XCTAssertEqual(try String(contentsOf: fileURL), "Stash me!") - } + // Only first file restored + #expect(FileManager.default.fileExists(atPath: file1.path) == true) + #expect(FileManager.default.fileExists(atPath: file2.path) == false) - func testStashPop() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-stash-pop", in: Self.directory) + // Both stashes still exist + #expect(try repository.stash.list().count == 2) + } - // Create mock commit + @Test("Apply on empty stash throws error", .tags(.error)) + func applyEmptyThrows() async throws { + let repository = mockRepository() try repository.mockCommit() - // Create a file - let fileURL = try URL(fileURLWithPath: "test.txt", relativeTo: repository.workingDirectory) - FileManager.default.createFile(atPath: fileURL.path, contents: Data("Stash me!".utf8)) + #expect(throws: SwiftGitXError.self) { + try repository.stash.apply() + } + } +} + +// MARK: - Pop Operations + +@Suite("Stash Collection - Pop Operations", .tags(.stash, .collection)) +final class StashPopTests: SwiftGitXTest { + @Test("Pop restores changes and removes stash entry") + func popRemovesEntry() async throws { + let repository = mockRepository() + try repository.mockCommit() - // Create a new stash entry + // Create and stash a file + let file = try repository.mockFile() try repository.stash.save(options: .includeUntracked) - XCTAssertEqual(try repository.stash.list().count, 1) - XCTAssertFalse(FileManager.default.fileExists(atPath: fileURL.path)) + #expect(FileManager.default.fileExists(atPath: file.path) == false) + #expect(try repository.stash.list().count == 1) - // Apply the stash entry + // Pop the stash try repository.stash.pop() - // List the stashes + // File restored, stash removed + #expect(FileManager.default.fileExists(atPath: file.path) == true) + #expect(try repository.stash.list().count == 0) + } + + @Test("Pop specific stash entry by reference") + func popSpecific() async throws { + let repository = mockRepository() + try repository.mockCommit() + + // Create two stashes + let file1 = try repository.mockFile(name: "first.txt") + try repository.stash.save(message: "First", options: .includeUntracked) + + _ = try repository.mockFile(name: "second.txt") + try repository.stash.save(message: "Second", options: .includeUntracked) + + // Pop the older stash (index 1) let stashes = try repository.stash.list() + try repository.stash.pop(stashes[1]) - // Check the stash entries - XCTAssertEqual(stashes.count, 0) // The stash should be removed - XCTAssertTrue(FileManager.default.fileExists(atPath: fileURL.path)) - XCTAssertEqual(try String(contentsOf: fileURL), "Stash me!") + // First file restored, only second stash remains + #expect(FileManager.default.fileExists(atPath: file1.path) == true) + #expect(try repository.stash.list().count == 1) + #expect(try repository.stash.list()[0].message == "On main: Second") } - func testStashDrop() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-stash-drop", in: Self.directory) - - // Create mock commit + @Test("Pop on empty stash throws error", .tags(.error)) + func popEmptyThrows() async throws { + let repository = mockRepository() try repository.mockCommit() - // Create a file - let fileURL = try URL(fileURLWithPath: "test.txt", relativeTo: repository.workingDirectory) - FileManager.default.createFile(atPath: fileURL.path, contents: Data("Stash me!".utf8)) + #expect(throws: SwiftGitXError.self) { + try repository.stash.pop() + } + } +} + +// MARK: - Drop Operations + +@Suite("Stash Collection - Drop Operations", .tags(.stash, .collection)) +final class StashDropTests: SwiftGitXTest { + @Test("Drop removes stash without applying changes") + func dropDiscards() async throws { + let repository = mockRepository() + try repository.mockCommit() - // Create a new stash entry + // Create and stash a file + let file = try repository.mockFile() try repository.stash.save(options: .includeUntracked) - // Drop the stash entry + // Drop the stash try repository.stash.drop() - // List the stash entries + // File still gone, stash removed + #expect(FileManager.default.fileExists(atPath: file.path) == false) + #expect(try repository.stash.list().count == 0) + } + + @Test("Drop specific stash entry by reference") + func dropSpecific() async throws { + let repository = mockRepository() + try repository.mockCommit() + + // Create two stashes + _ = try repository.mockFile() + try repository.stash.save(message: "First", options: .includeUntracked) + + _ = try repository.mockFile() + try repository.stash.save(message: "Second", options: .includeUntracked) + + #expect(try repository.stash.list().count == 2) + + // Drop the newer stash (index 0) let stashes = try repository.stash.list() + try repository.stash.drop(stashes[0]) + + // Only older stash remains + let remaining = try repository.stash.list() + #expect(remaining.count == 1) + #expect(remaining[0].message == "On main: First") + } + + @Test("Drop on empty stash throws error", .tags(.error)) + func dropEmptyThrows() async throws { + let repository = mockRepository() + try repository.mockCommit() - // Check the stash entries - XCTAssertEqual(stashes.count, 0) - XCTAssertFalse(FileManager.default.fileExists(atPath: fileURL.path)) + #expect(throws: SwiftGitXError.self) { + try repository.stash.drop() + } } } diff --git a/Tests/SwiftGitXTests/CollectionTests/TagCollectionTests.swift b/Tests/SwiftGitXTests/CollectionTests/TagCollectionTests.swift index 2adbd9b..d36bd5e 100644 --- a/Tests/SwiftGitXTests/CollectionTests/TagCollectionTests.swift +++ b/Tests/SwiftGitXTests/CollectionTests/TagCollectionTests.swift @@ -1,284 +1,372 @@ import SwiftGitX -import XCTest +import Testing -class TagCollectionTests: SwiftGitXTestCase { - func testTagLookupSubscript() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-tag-lookup-subscript", in: Self.directory) +// MARK: - Lookup Operations - // Create mock commit +@Suite("Tag Collection - Lookup Operations", .tags(.tag, .collection)) +final class TagLookupTests: SwiftGitXTest { + @Test("Lookup tag by subscript") + func lookupSubscript() async throws { + let repository = mockRepository() let commit = try repository.mockCommit() - // Create a new tag + // Create a tag try repository.tag.create(named: "v1.0.0", target: commit) - // Lookup the tag by name - let tag = try XCTUnwrap(repository.tag["v1.0.0"]) + // Lookup by subscript + let tag = try #require(repository.tag["v1.0.0"]) - // Check the tag properties - XCTAssertEqual(tag.name, "v1.0.0") - XCTAssertEqual(tag.fullName, "refs/tags/v1.0.0") + #expect(tag.name == "v1.0.0") + #expect(tag.fullName == "refs/tags/v1.0.0") - // The tag target is the commit - let tagTarget = try XCTUnwrap(tag.target as? Commit) - XCTAssertEqual(tagTarget, commit) + // Tag target is the commit + let tagTarget = try #require(tag.target as? Commit) + #expect(tagTarget == commit) } - func testTagLookupSubscriptFailure() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-tag-lookup-subscript-failure", in: Self.directory) - - // Create mock commit + @Test("Lookup non-existent tag returns nil") + func lookupSubscriptNotFound() async throws { + let repository = mockRepository() try repository.mockCommit() - XCTAssertNil(repository.tag["v1.0.0"]) + #expect(repository.tag["v1.0.0"] == nil) } - func testTagLookupAnnotated() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-tag-lookup-annotated", in: Self.directory) - - // Create mock commit + @Test("Get annotated tag") + func getAnnotated() async throws { + let repository = mockRepository() let commit = try repository.mockCommit() - // Create a new tag + // Create annotated tag with message try repository.tag.create(named: "v1.0.0", target: commit, message: "Initial release") - // Lookup the tag by name + // Lookup the tag let annotatedTag = try repository.tag.get(named: "v1.0.0") - // Check the tag properties - XCTAssertEqual(annotatedTag.name, "v1.0.0") - XCTAssertEqual(annotatedTag.fullName, "refs/tags/v1.0.0") - - // The tag target is the commit - let tagTarget = try XCTUnwrap(annotatedTag.target as? Commit) - XCTAssertEqual(tagTarget, commit) + #expect(annotatedTag.name == "v1.0.0") + #expect(annotatedTag.fullName == "refs/tags/v1.0.0") + #expect(annotatedTag.message == "Initial release") - XCTAssertEqual(annotatedTag.message, "Initial release") + // Tag target is the commit + let tagTarget = try #require(annotatedTag.target as? Commit) + #expect(tagTarget == commit) } - func testTagLookupLightweight() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-tag-lookup-lightweight", in: Self.directory) - - // Create mock commit + @Test("Get lightweight tag") + func getLightweight() async throws { + let repository = mockRepository() let commit = try repository.mockCommit() - // Create a new tag + // Create lightweight tag try repository.tag.create(named: "v1.0.0", target: commit, type: .lightweight) - // Lookup the tag by short name + // Lookup the tag let lightweightTag = try repository.tag.get(named: "v1.0.0") - // Check the tag properties - XCTAssertEqual(lightweightTag.name, "v1.0.0") - XCTAssertEqual(lightweightTag.fullName, "refs/tags/v1.0.0") + #expect(lightweightTag.name == "v1.0.0") + #expect(lightweightTag.fullName == "refs/tags/v1.0.0") - // Check if the tag id is the same as the blob id - XCTAssertEqual(lightweightTag.id, commit.id) - XCTAssertEqual(lightweightTag.id, lightweightTag.target.id) + // Lightweight tag ID matches commit ID + #expect(lightweightTag.id == commit.id) + #expect(lightweightTag.id == lightweightTag.target.id) - // Lightweight tag target is the commit - let lightweightTagTarget = try XCTUnwrap(lightweightTag.target as? Commit) - XCTAssertEqual(lightweightTagTarget, commit) + // Tag target is the commit + let lightweightTagTarget = try #require(lightweightTag.target as? Commit) + #expect(lightweightTagTarget == commit) - // Lightweight tag have no tagger and message - XCTAssertNil(lightweightTag.tagger) - XCTAssertNil(lightweightTag.message) + // Lightweight tags have no tagger or message + #expect(lightweightTag.tagger == nil) + #expect(lightweightTag.message == nil) } - func testTagList() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-tag-list", in: Self.directory) + @Test("Get non-existent tag throws error", .tags(.error)) + func getNotFoundThrows() async throws { + let repository = mockRepository() + try repository.mockCommit() + + #expect(throws: SwiftGitXError.self) { + try repository.tag.get(named: "non-existent") + } + } +} - // Check if the tag list is empty - XCTAssertTrue(try repository.tag.list().isEmpty) +// MARK: - List & Iterator Operations - // Create mock commit - let commit = try repository.mockCommit() +@Suite("Tag Collection - List & Iterator", .tags(.tag, .collection)) +final class TagListTests: SwiftGitXTest { + @Test("List returns empty array when no tags exist") + func listEmpty() async throws { + let repository = mockRepository() + try repository.mockCommit() - // Create some tags - let newTagNames = ["v1.0.0", "v1.0.1", "v1.0.2", "v1.0.3"] + let tags = try repository.tag.list() + #expect(tags.isEmpty) + } + + @Test("List returns all tags") + func listAll() async throws { + let repository = mockRepository() + let commit = try repository.mockCommit() - for name in newTagNames { + // Create tags + let tagNames = ["v1.0.0", "v1.0.1", "v1.0.2", "v1.0.3"] + for name in tagNames { try repository.tag.create(named: name, target: commit) } // List all tags let tags = try repository.tag.list() - // Check if the tag is in the list - XCTAssertEqual(tags.count, 4) - + #expect(tags.count == 4) for tag in tags { - XCTAssertTrue(newTagNames.contains(tag.name)) + #expect(tagNames.contains(tag.name)) } } - func testTagIterator() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-tag-iterator", in: Self.directory) + @Test("Iterate over tags") + func iterate() async throws { + let repository = mockRepository() - // Create mock commits - let commits = try (0..<5).map { index in - try repository.mockCommit(file: repository.mockFile(named: "README-\(index).md")) + // Create commits and tags + let commits = try (0..<5).map { _ in + try repository.mockCommit(file: repository.mockFile()) } - // Create some tags - let newTagNames = ["v1.0.0", "v1.0.1", "v1.0.2", "v1.0.3", "v1.0.4"] - - for (name, commit) in zip(newTagNames, commits) { + let tagNames = ["v1.0.0", "v1.0.1", "v1.0.2", "v1.0.3", "v1.0.4"] + for (name, commit) in zip(tagNames, commits) { try repository.tag.create(named: name, target: commit, message: "Release \(name)") } - // Iterate over the tags + // Iterate over tags for (tag, commit) in zip(repository.tag, commits) { - XCTAssertTrue(newTagNames.contains(tag.name)) - XCTAssertEqual("refs/tags/\(tag.name)", tag.fullName) - XCTAssertEqual(tag.target as? Commit, commit) - XCTAssertEqual(tag.message, "Release \(tag.name)") + #expect(tagNames.contains(tag.name)) + #expect(tag.fullName == "refs/tags/\(tag.name)") + #expect(tag.target as? Commit == commit) + #expect(tag.message == "Release \(tag.name)") } } +} - func testTagCreateAnnotated() throws { - // Create mock repository at the temporary directory - let repository = Repository.mock(named: "test-tag-create-annotated", in: Self.directory) +// MARK: - Create Operations - // Commit the changes +@Suite("Tag Collection - Create Operations", .tags(.tag, .collection)) +final class TagCreateTests: SwiftGitXTest { + @Test("Create annotated tag") + func createAnnotated() async throws { + let repository = mockRepository() let commit = try repository.mockCommit() - // Create a new tag + // Create annotated tag (default type) let annotatedTag = try repository.tag.create(named: "v1.0.0", target: repository.HEAD.target) - // Check the tag properties - XCTAssertEqual(annotatedTag.name, "v1.0.0") - XCTAssertEqual(annotatedTag.fullName, "refs/tags/v1.0.0") + #expect(annotatedTag.name == "v1.0.0") + #expect(annotatedTag.fullName == "refs/tags/v1.0.0") + #expect(annotatedTag.target.id == commit.id) + #expect(annotatedTag.message == nil) - // The tag target is the commit - let tagTarget = try XCTUnwrap(annotatedTag.target as? Commit) - XCTAssertEqual(tagTarget, commit) - - XCTAssertNil(annotatedTag.message) + // Tag target is the commit + let tagTarget = try #require(annotatedTag.target as? Commit) + #expect(tagTarget == commit) } - func testTagCreateLightweight() throws { - // Create mock repository at the temporary directory - let repository = Repository.mock(named: "test-tag-create-lightweight", in: Self.directory) - - // Commit the changes + @Test("Create lightweight tag") + func createLightweight() async throws { + let repository = mockRepository() let commit = try repository.mockCommit() - // Create a new tag + // Create lightweight tag let lightweightTag = try repository.tag.create(named: "v1.0.0", target: commit, type: .lightweight) - // Check the name of the tag - XCTAssertEqual(lightweightTag.name, "v1.0.0") - XCTAssertEqual(lightweightTag.fullName, "refs/tags/v1.0.0") + #expect(lightweightTag.name == "v1.0.0") + #expect(lightweightTag.fullName == "refs/tags/v1.0.0") + #expect(lightweightTag.target.id == commit.id) - // Lightweight tag have the same id as the target commit - XCTAssertEqual(lightweightTag.id, commit.id) - XCTAssertEqual(lightweightTag.id, lightweightTag.target.id) + // Lightweight tag has same ID as commit + #expect(lightweightTag.id == commit.id) + #expect(lightweightTag.id == lightweightTag.target.id) - // Lightweight tag target is the commit - let lightweightTagTarget = try XCTUnwrap(lightweightTag.target as? Commit) - XCTAssertEqual(lightweightTagTarget, commit) + // Tag target is the commit + let lightweightTagTarget = try #require(lightweightTag.target as? Commit) + #expect(lightweightTagTarget == commit) - // Lightweight tag have no tagger and message - XCTAssertNil(lightweightTag.tagger) - XCTAssertNil(lightweightTag.message) + // Lightweight tags have no tagger or message + #expect(lightweightTag.tagger == nil) + #expect(lightweightTag.message == nil) } - func testTagCreateLightweightPointingTree() throws { - // Create mock repository at the temporary directory - let repository = Repository.mock(named: "test-tag-create-lightweight-pointing-tree", in: Self.directory) - - // Create mock commit + @Test("Create lightweight tag pointing to tree") + func createLightweightPointingTree() async throws { + let repository = mockRepository() let commit = try repository.mockCommit() - // Get the tree of the commit + // Get tree from commit let tree = try commit.tree - // Create a new tag + // Create lightweight tag pointing to tree let lightweightTag = try repository.tag.create(named: "v1.0.0", target: tree, type: .lightweight) - // Check the name of the tag - XCTAssertEqual(lightweightTag.name, "v1.0.0") - XCTAssertEqual(lightweightTag.fullName, "refs/tags/v1.0.0") + #expect(lightweightTag.name == "v1.0.0") + #expect(lightweightTag.fullName == "refs/tags/v1.0.0") + #expect(lightweightTag.target.id == tree.id) - // Check if the tag id is the same as the tree id - XCTAssertEqual(lightweightTag.id, tree.id) - XCTAssertEqual(lightweightTag.id, lightweightTag.target.id) + // Tag ID matches tree ID + #expect(lightweightTag.id == tree.id) + #expect(lightweightTag.id == lightweightTag.target.id) - // Lightweight tag target is the tree - let lightweightTagTarget = try XCTUnwrap(lightweightTag.target as? Tree) - XCTAssertEqual(lightweightTagTarget, tree) + // Tag target is the tree + let lightweightTagTarget = try #require(lightweightTag.target as? Tree) + #expect(lightweightTagTarget == tree) - // Lightweight tag have no tagger and message - XCTAssertNil(lightweightTag.tagger) - XCTAssertNil(lightweightTag.message) + // Lightweight tags have no tagger or message + #expect(lightweightTag.tagger == nil) + #expect(lightweightTag.message == nil) } - func testTagCreateLightweightPointingBlob() throws { - // Create mock repository at the temporary directory - let repository = Repository.mock(named: "test-tag-create-lightweight-pointing-blob", in: Self.directory) - - // Create mock commit + @Test("Create lightweight tag pointing to blob") + func createLightweightPointingBlob() async throws { + let repository = mockRepository() let commit = try repository.mockCommit() - // Get the first blob from the commit + // Get first blob from commit's tree let tree = try commit.tree let blob: Blob = tree.entries.compactMap { try? repository.show(id: $0.id) }.first! - // Create a new tag + // Create lightweight tag pointing to blob let lightweightTag = try repository.tag.create(named: "v1.0.0", target: blob, type: .lightweight) - // Check the name of the tag - XCTAssertEqual(lightweightTag.name, "v1.0.0") - XCTAssertEqual(lightweightTag.fullName, "refs/tags/v1.0.0") + #expect(lightweightTag.name == "v1.0.0") + #expect(lightweightTag.fullName == "refs/tags/v1.0.0") + #expect(lightweightTag.target.id == blob.id) - // Check if the tag id is the same as the blob id - XCTAssertEqual(lightweightTag.id, blob.id) - XCTAssertEqual(lightweightTag.id, lightweightTag.target.id) + // Tag ID matches blob ID + #expect(lightweightTag.id == blob.id) + #expect(lightweightTag.id == lightweightTag.target.id) - // Lightweight tag target is the blob - let lightweightTagTarget = try XCTUnwrap(lightweightTag.target as? Blob) - XCTAssertEqual(lightweightTagTarget, blob) + // Tag target is the blob + let lightweightTagTarget = try #require(lightweightTag.target as? Blob) + #expect(lightweightTagTarget == blob) - // Lightweight tag have no tagger and message - XCTAssertNil(lightweightTag.tagger) - XCTAssertNil(lightweightTag.message) + // Lightweight tags have no tagger or message + #expect(lightweightTag.tagger == nil) + #expect(lightweightTag.message == nil) } - func testTagCreateLightweightPointingTag() throws { - // Create mock repository at the temporary directory - let repository = Repository.mock(named: "test-tag-create-lightweight-pointing-tag", in: Self.directory) - - // Create mock commit + @Test("Create lightweight tag pointing to tag") + func createLightweightPointingTag() async throws { + let repository = mockRepository() let commit = try repository.mockCommit() - // Create a new tag + // Create an annotated tag first let annotatedTag = try repository.tag.create(named: "initial-tag", target: commit) - // Create a new tag + // Create lightweight tag pointing to the annotated tag let lightweightTag = try repository.tag.create(named: "v1.0.0", target: annotatedTag, type: .lightweight) - // Check the name of the tag - XCTAssertEqual(lightweightTag.name, "v1.0.0") - XCTAssertEqual(lightweightTag.fullName, "refs/tags/v1.0.0") + #expect(lightweightTag.name == "v1.0.0") + #expect(lightweightTag.fullName == "refs/tags/v1.0.0") + #expect(lightweightTag.target.id == annotatedTag.id) + + // Tag ID matches annotated tag ID + #expect(lightweightTag.id == annotatedTag.id) + #expect(lightweightTag.id == lightweightTag.target.id) + + // Tag target is the annotated tag + let lightweightTagTarget = try #require(lightweightTag.target as? SwiftGitX.Tag) + #expect(lightweightTagTarget == annotatedTag) + + // Lightweight tags have no tagger or message + #expect(lightweightTag.tagger == nil) + #expect(lightweightTag.message == nil) + } + + @Test("Create annotated tag with message") + func createAnnotatedWithMessage() async throws { + let repository = mockRepository() + let commit = try repository.mockCommit() + + // Create annotated tag with message + let tag = try repository.tag.create( + named: "v1.0.0", + target: commit, + message: "Release version 1.0.0" + ) + + #expect(tag.name == "v1.0.0") + #expect(tag.fullName == "refs/tags/v1.0.0") + #expect(tag.target.id == commit.id) + #expect(tag.message == "Release version 1.0.0") + #expect(tag.tagger != nil) + } + + @Test("Create annotated tag with custom tagger") + func createAnnotatedWithCustomTagger() async throws { + let repository = mockRepository() + let commit = try repository.mockCommit() + + // Create custom tagger signature + let customTagger = Signature( + name: "Custom Tagger", + email: "tagger@example.com" + ) + + // Create annotated tag with custom tagger + let tag = try repository.tag.create( + named: "v1.0.0", + target: commit, + tagger: customTagger, + message: "Tagged by custom tagger" + ) + + #expect(tag.name == "v1.0.0") + #expect(tag.message == "Tagged by custom tagger") + #expect(tag.target.id == commit.id) + + let tagger = try #require(tag.tagger) + #expect(tagger.name == "Custom Tagger") + #expect(tagger.email == "tagger@example.com") + } - // Check if the tag id is the same as the blob id - XCTAssertEqual(lightweightTag.id, annotatedTag.id) - XCTAssertEqual(lightweightTag.id, lightweightTag.target.id) + @Test("Create tag with force overwrites existing") + func createWithForceOverwrites() async throws { + let repository = mockRepository() + let commit1 = try repository.mockCommit() + let commit2 = try repository.mockCommit() + + // Create initial tag + let originalTag = try repository.tag.create(named: "v1.0.0", target: commit1, message: "Original") + #expect(originalTag.message == "Original") + #expect(originalTag.target.id == commit1.id) + + // Overwrite with force + let newTag = try repository.tag.create( + named: "v1.0.0", + target: commit2, + message: "Overwritten", + force: true + ) + + #expect(newTag.name == "v1.0.0") + #expect(newTag.message == "Overwritten") + #expect(newTag.target.id == commit2.id) + + // Verify the tag now points to commit2 + let tagTarget = try #require(newTag.target as? Commit) + #expect(tagTarget == commit2) + } - // Lightweight tag target is the annotated tag - let lightweightTagTarget = try XCTUnwrap(lightweightTag.target as? Tag) - XCTAssertEqual(lightweightTagTarget, annotatedTag) + @Test("Create existing tag without force throws error", .tags(.error)) + func createExistingTagThrows() async throws { + let repository = mockRepository() + let commit = try repository.mockCommit() - // Lightweight tag have no tagger and message - XCTAssertNil(lightweightTag.tagger) - XCTAssertNil(lightweightTag.message) + // Create initial tag + try repository.tag.create(named: "v1.0.0", target: commit) + + // Try to create again without force + #expect(throws: SwiftGitXError.self) { + try repository.tag.create(named: "v1.0.0", target: commit) + } } } diff --git a/Tests/SwiftGitXTests/ModelTests/OIDTests.swift b/Tests/SwiftGitXTests/ModelTests/OIDTests.swift new file mode 100644 index 0000000..391a23d --- /dev/null +++ b/Tests/SwiftGitXTests/ModelTests/OIDTests.swift @@ -0,0 +1,69 @@ +import Testing +import libgit2 + +@testable import SwiftGitX + +@Suite("OID Tests", .tags(.oid, .model)) +final class OIDTests: SwiftGitXTest { + @Test("Initialize OID from hex string") + func initFromHex() async throws { + let shaHex = "42a02b346bb0fb0db7eff3cffeb3c70babbd2045" + let oid = try OID(hex: shaHex) + + #expect(oid.hex == shaHex) + } + + @Test("OID abbreviated returns first 8 characters") + func abbreviated() async throws { + let shaHex = "42a02b346bb0fb0db7eff3cffeb3c70babbd2045" + let oid = try OID(hex: shaHex) + + #expect(oid.abbreviated == "42a02b34") + } + + @Test("OID round-trip conversion to raw and back") + func roundTripRaw() async throws { + let shaHex = "42a02b346bb0fb0db7eff3cffeb3c70babbd2045" + let oid = try OID(hex: shaHex) + + let raw = oid.raw + let converted = OID(raw: raw) + + #expect(oid == converted) + #expect(oid.abbreviated == converted.abbreviated) + #expect(oid.hex == converted.hex) + } + + @Test("OID matches libgit2 raw OID") + func matchesLibgit2Raw() async throws { + let shaHex = "42a02b346bb0fb0db7eff3cffeb3c70babbd2045" + let oid = try OID(hex: shaHex) + + var rawOID = git_oid() + git_oid_fromstr(&rawOID, shaHex) + + #expect(oid == OID(raw: rawOID)) + } + + @Test("Zero OID has all zeros") + func zeroOID() async throws { + let zeroOID = OID.zero + + #expect(zeroOID.hex == "0000000000000000000000000000000000000000") + #expect(zeroOID.abbreviated == "00000000") + } + + @Test("Zero OID is recognized by libgit2") + func zeroOIDLibgit2() async throws { + var zeroOIDRaw = OID.zero.raw + + #expect(git_oid_is_zero(&zeroOIDRaw) == 1) + } + + @Test("Zero OID equals .zero static property") + func zeroOIDEquality() async throws { + let zeroOID = OID.zero + + #expect(zeroOID == .zero) + } +} diff --git a/Tests/SwiftGitXTests/ModelTests/SignatureTests.swift b/Tests/SwiftGitXTests/ModelTests/SignatureTests.swift new file mode 100644 index 0000000..1697873 --- /dev/null +++ b/Tests/SwiftGitXTests/ModelTests/SignatureTests.swift @@ -0,0 +1,316 @@ +import Foundation +import Testing + +@testable import SwiftGitX + +// MARK: - Initialization Tests + +@Suite("Signature - Initialization", .tags(.signature, .model)) +final class SignatureInitTests: SwiftGitXTest { + @Test("Initialize with all parameters") + func initWithAllParameters() async throws { + let date = Date.now + let timezone = TimeZone(identifier: "Europe/Istanbul")! + + let signature = Signature( + name: "John Doe", + email: "john@example.com", + date: date, + timezone: timezone + ) + + #expect(signature.name == "John Doe") + #expect(signature.email == "john@example.com") + #expect(signature.date == date) + #expect(signature.timezone == timezone) + } + + @Test("Initialize with default date and timezone") + func initWithDefaults() async throws { + let before = Date.now + + let signature = Signature( + name: "Jane Doe", + email: "jane@example.com" + ) + + let after = Date.now + + #expect(signature.name == "Jane Doe") + #expect(signature.email == "jane@example.com") + + // Date should be between before and after + #expect(signature.date >= before) + #expect(signature.date <= after) + + // Timezone should be current + #expect(signature.timezone == TimeZone.current) + } + + @Test("Initialize with specific date") + func initWithSpecificDate() async throws { + let specificDate = Date(timeIntervalSince1970: 1_700_000_000) + + let signature = Signature( + name: "Test User", + email: "test@example.com", + date: specificDate + ) + + #expect(signature.date == specificDate) + #expect(signature.timezone == TimeZone.current) + } + + @Test("Initialize with specific timezone") + func initWithSpecificTimezone() async throws { + let utc = TimeZone(identifier: "UTC")! + + let signature = Signature( + name: "UTC User", + email: "utc@example.com", + timezone: utc + ) + + #expect(signature.timezone == utc) + } +} + +// MARK: - Equality Tests + +@Suite("Signature - Equality & Hashing", .tags(.signature, .model)) +final class SignatureEqualityTests: SwiftGitXTest { + @Test("Equal signatures are equal") + func equalSignatures() async throws { + let date = Date.now + let timezone = TimeZone.current + + let signature1 = Signature(name: "John", email: "john@example.com", date: date, timezone: timezone) + let signature2 = Signature(name: "John", email: "john@example.com", date: date, timezone: timezone) + + #expect(signature1 == signature2) + } + + @Test("Different names are not equal") + func differentNames() async throws { + let date = Date.now + + let signature1 = Signature(name: "John", email: "john@example.com", date: date) + let signature2 = Signature(name: "Jane", email: "john@example.com", date: date) + + #expect(signature1 != signature2) + } + + @Test("Different emails are not equal") + func differentEmails() async throws { + let date = Date.now + + let signature1 = Signature(name: "John", email: "john@example.com", date: date) + let signature2 = Signature(name: "John", email: "jane@example.com", date: date) + + #expect(signature1 != signature2) + } + + @Test("Different dates are not equal") + func differentDates() async throws { + let signature1 = Signature(name: "John", email: "john@example.com", date: Date.now) + let signature2 = Signature(name: "John", email: "john@example.com", date: Date.now.addingTimeInterval(1)) + + #expect(signature1 != signature2) + } + + @Test("Equal signatures have same hash") + func hashEquality() async throws { + let date = Date.now + let timezone = TimeZone.current + + let signature1 = Signature(name: "John", email: "john@example.com", date: date, timezone: timezone) + let signature2 = Signature(name: "John", email: "john@example.com", date: date, timezone: timezone) + + #expect(signature1.hashValue == signature2.hashValue) + } + + @Test("Signatures can be used in Set") + func usableInSet() async throws { + let date = Date.now + + let signature1 = Signature(name: "John", email: "john@example.com", date: date) + let signature2 = Signature(name: "John", email: "john@example.com", date: date) + let signature3 = Signature(name: "Jane", email: "jane@example.com", date: date) + + var set: Set = [] + set.insert(signature1) + set.insert(signature2) + set.insert(signature3) + + // signature1 and signature2 are equal, so set should have 2 elements + #expect(set.count == 2) + } +} + +// MARK: - Default Signature Tests + +@Suite("Signature - Default in Repository", .tags(.signature, .model)) +final class SignatureDefaultTests: SwiftGitXTest { + @Test("Get default signature from repository") + func defaultSignature() async throws { + let repository = mockRepository() + + // Set git config for user + try repository.config.set("user.name", to: "Test User") + try repository.config.set("user.email", to: "test@example.com") + + // Get default signature + let signature = try Signature.default(in: repository) + + #expect(signature.name == "Test User") + #expect(signature.email == "test@example.com") + } + + @Test("Default signature uses current date") + func defaultSignatureDate() async throws { + let repository = mockRepository() + + try repository.config.set("user.name", to: "Test User") + try repository.config.set("user.email", to: "test@example.com") + + // Truncate to seconds since git_signature stores time as integer seconds + let before = floor(Date.now.timeIntervalSince1970) + let signature = try Signature.default(in: repository) + let after = ceil(Date.now.timeIntervalSince1970) + + #expect(signature.date.timeIntervalSince1970 >= before) + #expect(signature.date.timeIntervalSince1970 <= after) + } +} + +// MARK: - Raw Conversion Tests + +@Suite("Signature - Raw Conversion", .tags(.signature, .model)) +final class SignatureRawTests: SwiftGitXTest { + @Test("Convert to raw preserves name and email") + func rawPreservesNameAndEmail() async throws { + let signature = Signature( + name: "Test User", + email: "test@example.com" + ) + + let raw = try signature.raw + + #expect(String(cString: raw.name) == "Test User") + #expect(String(cString: raw.email) == "test@example.com") + } + + @Test("Convert to raw preserves date") + func rawPreservesDate() async throws { + let specificDate = Date(timeIntervalSince1970: 1_700_000_000) + + let signature = Signature( + name: "Test User", + email: "test@example.com", + date: specificDate + ) + + let raw = try signature.raw + + #expect(raw.when.time == 1_700_000_000) + } + + @Test("Convert to raw preserves timezone offset") + func rawPreservesTimezone() async throws { + // UTC+3 timezone (180 minutes offset) + let timezone = TimeZone(secondsFromGMT: 3 * 60 * 60)! + + let signature = Signature( + name: "Test User", + email: "test@example.com", + timezone: timezone + ) + + let raw = try signature.raw + + // Offset is stored in minutes + #expect(raw.when.offset == 180) + } + + @Test("Convert to raw with negative timezone offset") + func rawPreservesNegativeTimezone() async throws { + // UTC-5 timezone (-300 minutes offset) + let timezone = TimeZone(secondsFromGMT: -5 * 60 * 60)! + + let signature = Signature( + name: "Test User", + email: "test@example.com", + timezone: timezone + ) + + let raw = try signature.raw + + #expect(raw.when.offset == -300) + } + + @Test("Round-trip conversion preserves all values") + func roundTripConversion() async throws { + let originalDate = Date(timeIntervalSince1970: 1_700_000_000) + let originalTimezone = TimeZone(secondsFromGMT: 2 * 60 * 60)! + + let original = Signature( + name: "Round Trip User", + email: "roundtrip@example.com", + date: originalDate, + timezone: originalTimezone + ) + + // Convert to raw and back + let raw = try original.raw + let converted = Signature(raw: raw) + + #expect(converted.name == original.name) + #expect(converted.email == original.email) + #expect(converted.date == original.date) + #expect(converted.timezone == original.timezone) + } + + @Test("Round-trip with UTC timezone") + func roundTripUTC() async throws { + let utc = TimeZone(identifier: "UTC")! + + let original = Signature( + name: "UTC User", + email: "utc@example.com", + date: Date(timeIntervalSince1970: 1_600_000_000), + timezone: utc + ) + + let raw = try original.raw + let converted = Signature(raw: raw) + + #expect(converted.timezone.secondsFromGMT() == 0) + } + + @Test("Signature used in tag preserves values") + func signatureInTag() async throws { + let repository = mockRepository() + let commit = try repository.mockCommit() + + let customTagger = Signature( + name: "Tag Creator", + email: "tagger@example.com", + date: Date(timeIntervalSince1970: 1_700_000_000), + timezone: TimeZone(secondsFromGMT: 60 * 60)! // UTC+1 + ) + + // Create tag with custom tagger + let tag = try repository.tag.create( + named: "v1.0.0", + target: commit, + tagger: customTagger, + message: "Test tag" + ) + + // Retrieve and verify + let tagger = try #require(tag.tagger) + #expect(tagger.name == "Tag Creator") + #expect(tagger.email == "tagger@example.com") + #expect(tagger.date == Date(timeIntervalSince1970: 1_700_000_000)) + } +} diff --git a/Tests/SwiftGitXTests/ObjectTests.swift b/Tests/SwiftGitXTests/ObjectTests.swift deleted file mode 100644 index 70baa33..0000000 --- a/Tests/SwiftGitXTests/ObjectTests.swift +++ /dev/null @@ -1,123 +0,0 @@ -import XCTest -import libgit2 - -@testable import SwiftGitX - -final class ObjectTests: SwiftGitXTestCase { - func testOID() throws { - // Test OID hex initialization - let shaHex = "42a02b346bb0fb0db7eff3cffeb3c70babbd2045" - let oid = try OID(hex: shaHex) - - // Check if the OID hex is correct - XCTAssertEqual(oid.hex, shaHex) - - let raw = oid.raw - XCTAssertEqual(oid, OID(raw: raw)) - XCTAssertEqual(oid.abbreviated, OID(raw: raw).abbreviated) - XCTAssertEqual(oid.hex, OID(raw: raw).hex) - - // Check if the OID abbreviated is correct - let abbreviatedSHA = "42a02b34" - XCTAssertEqual(oid.abbreviated, abbreviatedSHA) - - // Check if the OID raw is correct - var rawOID = git_oid() - git_oid_fromstr(&rawOID, shaHex) - - XCTAssertEqual(oid, OID(raw: rawOID)) - - // Test OID is zero - let zeroOID = OID.zero - - XCTAssertEqual(zeroOID.hex, "0000000000000000000000000000000000000000") - XCTAssertEqual(zeroOID.abbreviated, "00000000") - - var zeroOIDRaw = zeroOID.raw - XCTAssertEqual(git_oid_is_zero(&zeroOIDRaw), 1) - - XCTAssertEqual(zeroOID, .zero) - } - - func testCommit() throws { - // Create mock repository at the temporary directory - let repository = Repository.mock(named: "test-object-commit", in: Self.directory) - - // Create a new file in the repository - let file = try repository.workingDirectory.appending(component: "README.md") - FileManager.default.createFile(atPath: file.path, contents: nil) - - // Add the file to the index - XCTAssertNoThrow(try repository.add(file: file)) - - // Commit the changes - let initialCommit = try repository.commit(message: "Initial commit") - - // TODO: Get default signature - - XCTAssertEqual(initialCommit.id, try repository.HEAD.target.id) - XCTAssertEqual(initialCommit.message, "Initial commit") - - // Check if the commit has no parent - XCTAssertEqual(try initialCommit.parents.count, 0) - - // Add content to the file - try Data("Hello, World!".utf8).write(to: file) - - // Add the file to the index - XCTAssertNoThrow(try repository.add(path: "README.md")) - - // Commit the changes - let commit = try repository.commit(message: "Add content to README.md") - - // Check if the commit has the correct parent - XCTAssertEqual(try commit.parents.count, 1) - - let parentCommit: Commit = try repository.show(id: commit.parents.first!.id) - XCTAssertEqual(parentCommit, initialCommit) - } - - func testTagAnnotated() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-object-tag-annotated", in: Self.directory) - - // Commit the changes - let commit = try repository.mockCommit() - - // Create a new tag - let tag = try repository.tag.create( - named: "v1.0.0", target: commit, message: "Initial release" - ) - - // Check if the tag is the same - XCTAssertEqual(tag.name, "v1.0.0") - XCTAssertEqual(tag.fullName, "refs/tags/v1.0.0") - - XCTAssertEqual(tag.target.id, commit.id) - XCTAssertEqual(tag.message, "Initial release") - - // TODO: Check tagger signature - } - - func testTagLightweight() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-object-tag-lightweight", in: Self.directory) - - // Commit the changes - let commit = try repository.mockCommit() - - // Create a new tag - let tag = try repository.tag.create(named: "v1.0.0", target: commit, type: .lightweight) - - // Check the tag properties - XCTAssertEqual(tag.name, "v1.0.0") - XCTAssertEqual(tag.fullName, "refs/tags/v1.0.0") - - XCTAssertEqual(tag.id, commit.id) - XCTAssertEqual(tag.id, tag.target.id) - XCTAssertEqual(tag.target.id, commit.id) - - XCTAssertNil(tag.tagger) - XCTAssertNil(tag.message) - } -} diff --git a/Tests/SwiftGitXTests/PerformanceTests/RepositoryPerformanceTests.swift b/Tests/SwiftGitXTests/PerformanceTests/RepositoryPerformanceTests.swift deleted file mode 100644 index cef7c51..0000000 --- a/Tests/SwiftGitXTests/PerformanceTests/RepositoryPerformanceTests.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// RepositoryPerformanceTests.swift -// -// -// Created by İbrahim Çetin on 21.04.2024. -// - -import SwiftGitX -import XCTest - -final class RepositoryPerformanceTests: SwiftGitXTestCase { - private let options: XCTMeasureOptions = { - let options = XCTMeasureOptions.default - - options.invocationOptions = [.manuallyStart, .manuallyStop] - options.iterationCount = 10 - - return options - }() - - func testPerformanceAdd() throws { - // Create a repository - let repository = Repository.mock(named: "test-performance-add", in: Self.directory) - - measure(options: options) { - do { - let file = try repository.mockFile(named: UUID().uuidString) - - // Measure the time it takes to add a file - startMeasuring() - try repository.add(file: file) - stopMeasuring() - } catch { - XCTFail(error.localizedDescription) - } - } - } - - func testPerformanceCommit() throws { - // Create a repository - let repository = Repository.mock(named: "test-performance-commit", in: Self.directory) - - measure(options: options) { - do { - // Add a file to the index - try repository.add(file: repository.mockFile(named: UUID().uuidString)) - - // Measure the time it takes to commit the file - startMeasuring() - try repository.commit(message: "Commit message") - stopMeasuring() - } catch { - XCTFail(error.localizedDescription) - } - } - } -} diff --git a/Tests/SwiftGitXTests/RepositoryTests/RepositoryAddTests.swift b/Tests/SwiftGitXTests/RepositoryTests/RepositoryAddTests.swift new file mode 100644 index 0000000..8d9a096 --- /dev/null +++ b/Tests/SwiftGitXTests/RepositoryTests/RepositoryAddTests.swift @@ -0,0 +1,20 @@ +import SwiftGitX +import Testing + +@Suite("Repository - Add", .tags(.repository, .operation, .add)) +final class RepositoryAddTests: SwiftGitXTest { + @Test("Add file to index") + func addFile() async throws { + let repository = mockRepository() + + // Create a file + let file = try repository.mockFile() + + // Add to index + try repository.add(file: file) + + // Verify status + let status = try repository.status(file: file) + #expect(status == [.indexNew]) + } +} diff --git a/Tests/SwiftGitXTests/RepositoryTests/RepositoryCloneTests.swift b/Tests/SwiftGitXTests/RepositoryTests/RepositoryCloneTests.swift new file mode 100644 index 0000000..7be9e77 --- /dev/null +++ b/Tests/SwiftGitXTests/RepositoryTests/RepositoryCloneTests.swift @@ -0,0 +1,121 @@ +import Foundation +import SwiftGitX +import Testing + +@Suite("Repository - Clone", .tags(.repository, .operation, .clone)) +final class RepositoryCloneTests: SwiftGitXTest { + @Test("Repository clone") + func repositoryClone() async throws { + // Create a temporary URL for the source repository + let source = URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! + + // Create a temporary directory for the destination repository + let directory = mockDirectory() + + // Perform the clone operation + _ = try await Repository.clone(from: source, to: directory) + + // Check if the destination repository exists + #expect(FileManager.default.fileExists(atPath: directory.path)) + + // Check if the repository opens without any errors + _ = try Repository(at: directory) + } + + @Test("Repository clone cancellation") + func repositoryCloneCancellation() async throws { + // Create a temporary URL for the source repository + let source = URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! + + // Create a temporary directory for the destination repository + let directory = mockDirectory() + + // Perform the clone operation + let task = Task { + try await Repository.clone(from: source, to: directory) + } + + // Cancel the task + task.cancel() + + // Wait for the task to complete + let result = await task.result + + // Check if the task is cancelled + #expect(task.isCancelled) + + // Check if the task result is a failure + guard case .failure = result else { + Issue.record("The task should be cancelled.") + return + } + + // Check if the destination repository exists + #expect(FileManager.default.fileExists(atPath: directory.path) == false) + } + + @Test("Repository clone with progress") + func repositoryCloneWithProgress() async throws { + // Create source URL for the repository + let source = URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! + + // Create a temporary directory for the destination repository + let directory = mockDirectory() + + var progressCompleted = false + + // Perform the clone operation + _ = try await Repository.clone(from: source, to: directory) { progress in + guard progress.indexedDeltas == progress.totalDeltas else { return } + guard progress.receivedObjects == progress.totalObjects else { return } + guard progress.indexedObjects == progress.totalObjects else { return } + + progressCompleted = true + } + + // Check if the progress completed + #expect(progressCompleted) + + // Check if the destination repository exists + #expect(FileManager.default.fileExists(atPath: directory.path)) + + // Check if the repository opens without any errors + _ = try Repository(at: directory) + } + + @Test("Repository clone with progress cancellation") + func repositoryCloneWithProgressCancellation() async throws { + // Create source URL for the repository + let source = URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! + + // Create a temporary directory for the destination repository + let directory = mockDirectory() + + // Create a task for the clone operation + let task = Task { + let repository = try await Repository.clone(from: source, to: directory) { progress in + print(progress) + } + + return repository + } + + // Cancel the task + task.cancel() + + // Wait for the task to complete (shouldn't wait because cancelled) + let result = await task.result + + // Check if the task is cancelled + #expect(task.isCancelled) + + // Check if the task result is a failure + guard case .failure = result else { + Issue.record("The task should be cancelled.") + return + } + + // Check if the destination repository exists + #expect(FileManager.default.fileExists(atPath: directory.path) == false) + } +} diff --git a/Tests/SwiftGitXTests/RepositoryTests/RepositoryCommitTests.swift b/Tests/SwiftGitXTests/RepositoryTests/RepositoryCommitTests.swift new file mode 100644 index 0000000..7a893f1 --- /dev/null +++ b/Tests/SwiftGitXTests/RepositoryTests/RepositoryCommitTests.swift @@ -0,0 +1,277 @@ +import Foundation +import SwiftGitX +import Testing + +// MARK: - Basic Commit Operations + +@Suite("Repository - Commit", .tags(.repository, .operation, .commit)) +final class RepositoryCommitTests: SwiftGitXTest { + @Test("Commit staged changes") + func commitStagedChanges() async throws { + let repository = mockRepository() + + // Create and stage a file + let file = try repository.mockFile() + try repository.add(file: file) + + // Commit + let commit = try repository.commit(message: "Initial commit") + + // Verify HEAD points to commit + let headCommit = try #require(repository.HEAD.target as? Commit) + #expect(commit == headCommit) + } + + @Test("Initial commit has no parents") + func initialCommitNoParents() async throws { + let repository = mockRepository() + + // Create initial commit + try repository.add(file: repository.mockFile()) + let initialCommit = try repository.commit(message: "Initial commit") + + #expect(initialCommit.id == (try repository.HEAD.target.id)) + #expect(try initialCommit.parents.isEmpty) + } + + @Test("Commit has parent") + func commitHasParent() async throws { + let repository = mockRepository() + + // Create initial commit + let initialCommit = try repository.mockCommit() + + // Create second commit + try repository.add(file: repository.mockFile()) + let secondCommit = try repository.commit(message: "Second commit") + + // Verify parent + let parents = try secondCommit.parents + #expect(parents.count == 1) + #expect(parents.first == initialCommit) + } + + @Test("Commit chain has correct parents") + func commitChain() async throws { + let repository = mockRepository() + + // Create chain of 5 commits + let commits = try (0..<5).map { _ in try repository.mockCommit() } + + // Verify each commit's parent (except first) + for i in 1..= before.timeIntervalSince1970 - 1) + #expect(commit.date.timeIntervalSince1970 <= after.timeIntervalSince1970 + 1) + } + + @Test("Commit has correct type") + func commitHasCorrectType() async throws { + let repository = mockRepository() + + try repository.add(file: repository.mockFile()) + let commit = try repository.commit(message: "Test commit") + + #expect(commit.type == .commit) + } +} + +// MARK: - Commit Tree + +@Suite("Repository - Commit Tree", .tags(.repository, .operation, .commit)) +final class RepositoryCommitTreeTests: SwiftGitXTest { + @Test("Commit tree contains committed file") + func commitTreeContainsFile() async throws { + let repository = mockRepository() + + // Create and commit a file + try repository.add(file: repository.mockFile()) + let commit = try repository.commit(message: "Add README") + + // Get tree and verify file exists + let tree = try commit.tree + let entry = tree.entries.first { $0.name == "file-1.txt" } + + #expect(entry != nil) + #expect(entry?.type == .blob) + } + + @Test("Commit tree reflects all staged files") + func commitTreeReflectsAllFiles() async throws { + let repository = mockRepository() + + // Create multiple files + let files = try (0..<3).map { _ in try repository.mockFile() } + + try repository.add(files: files) + let commit = try repository.commit(message: "Add files") + + // Verify all files are in tree + let tree = try commit.tree + let fileNames = tree.entries.map(\.name).sorted() + + #expect(fileNames == files.map(\.lastPathComponent)) + } +} + +// MARK: - Commit Options + +@Suite("Repository - Commit Options", .tags(.repository, .operation, .commit)) +final class RepositoryCommitOptionsTests: SwiftGitXTest { + @Test("Default options require staged changes") + func defaultOptionsRequireChanges() async throws { + let repository = mockRepository() + try repository.mockCommit() + + // Default options should fail with no changes + #expect(throws: SwiftGitXError.self) { + try repository.commit(message: "No changes", options: .default) + } + } + + @Test("allowEmpty option permits empty commit") + func allowEmptyOption() async throws { + let repository = mockRepository() + try repository.mockCommit() + + // allowEmpty should succeed + let emptyCommit = try repository.commit(message: "Empty commit", options: .allowEmpty) + + #expect(emptyCommit.message == "Empty commit") + } + + @Test("Custom CommitOptions with allowEmpty") + func customCommitOptions() async throws { + let repository = mockRepository() + try repository.mockCommit() + + // Create custom options + let options = CommitOptions(allowEmpty: true) + let commit = try repository.commit(message: "Custom options", options: options) + + #expect(commit.message == "Custom options") + } +} + +// MARK: - Commit Error Cases + +@Suite("Repository - Commit Errors", .tags(.repository, .operation, .commit, .error)) +final class RepositoryCommitErrorTests: SwiftGitXTest { + @Test("Commit with no staged changes throws error") + func noStagedChangesThrows() async throws { + let repository = mockRepository() + try repository.mockCommit() + + let error = #expect(throws: SwiftGitXError.self) { + try repository.commit(message: "No changes") + } + + #expect(error?.code == .unchanged) + #expect(error?.category == .repository) + #expect(error?.message == "no changes are staged for commit") + } + + @Test("Commit on empty repository with no staged files throws") + func emptyRepositoryNoStagedThrows() async throws { + let repository = mockRepository() + + // No files staged, no initial commit + #expect(throws: SwiftGitXError.self) { + try repository.commit(message: "Should fail") + } + } +} diff --git a/Tests/SwiftGitXTests/RepositoryTests/RepositoryDiffTests.swift b/Tests/SwiftGitXTests/RepositoryTests/RepositoryDiffTests.swift index 9d92a00..c1c58ca 100644 --- a/Tests/SwiftGitXTests/RepositoryTests/RepositoryDiffTests.swift +++ b/Tests/SwiftGitXTests/RepositoryTests/RepositoryDiffTests.swift @@ -1,58 +1,42 @@ +import Foundation import SwiftGitX -import XCTest +import Testing -final class RepositoryDiffTests: SwiftGitXTestCase { - /// This test creates a commit and a working tree change (there is no staged change). - func testDiffHEADToWorkingTree() throws { - // Create a mock repository at the temporary directory - let repository = Repository.mock(named: "test-diff-head-working-tree", in: Self.directory) +// MARK: - Diff HEAD to Working Tree + +@Suite("Repository - Diff HEAD to Working Tree", .tags(.repository, .operation, .diff)) +final class RepositoryDiffHEADToWorkingTreeTests: SwiftGitXTest { + @Test("Diff HEAD to working tree") + func diffHEADToWorkingTree() async throws { + let repository = mockRepository() // Create a commit - let file = try repository.mockFile(named: "README.md", content: "The commit content!\n") - try repository.mockCommit(file: file) + let file1 = try repository.mockFile() + try repository.mockCommit(file: file1) // Update the file content - try Data("The working tree content!\n".utf8).write(to: file) + try Data("The working tree content!\n".utf8).write(to: file1) // Get the diff between HEAD and the working tree let diff = try repository.diff() // Check if the diff count is correct - XCTAssertEqual(diff.patches[0].hunks.count, 1) + #expect(diff.patches[0].hunks.count == 1) let hunk = diff.patches[0].hunks[0] // Check the hunk lines - XCTAssertEqual(hunk.lines.count, 2) - XCTAssertEqual(hunk.lines[0].type, .deletion) - XCTAssertEqual(hunk.lines[0].content, "The commit content!\n") - - XCTAssertEqual(hunk.lines[1].type, .addition) - XCTAssertEqual(hunk.lines[1].content, "The working tree content!\n") - } - - /// This func creates base state for `testDiffHEADToWorkingTree_Staged`, `testDiffHEADToIndex` and - /// `testDiffHEADToWorkingTreeWithIndex`. It creates a commit, a staged change and a working tree change. - func createBaseStateForDiffHEAD(_ repository: Repository) throws { - // Create a file - let file = try repository.mockFile(named: "README.md", content: "The commit content!\n") - - // Create a commit - try repository.mockCommit(file: file) + #expect(hunk.lines.count == 2) + #expect(hunk.lines[0].type == .deletion) + #expect(hunk.lines[0].content == "File 1 content\n") - // Update the file content and add the file - try Data("The index content!\n".utf8).write(to: file) - try repository.add(file: file) - - // Update the file content - try Data("\nThe working tree content!\n".utf8).write(to: file) + #expect(hunk.lines[1].type == .addition) + #expect(hunk.lines[1].content == "The working tree content!\n") } - /// This test creates a commit, a staged change and a working tree change. - /// This test should compare the staged change with the working tree change. - func testDiffHEADToWorkingTree_Staged() throws { - // Create a mock repository at the temporary directory - let repository = Repository.mock(named: "test-diff-head-working-tree_staged", in: Self.directory) + @Test("Diff HEAD to working tree with staged changes") + func diffHEADToWorkingTreeStaged() async throws { + let repository = mockRepository() // Create a base state for the test try createBaseStateForDiffHEAD(repository) @@ -61,24 +45,24 @@ final class RepositoryDiffTests: SwiftGitXTestCase { let diff = try repository.diff() // Check if the diff count is correct - XCTAssertEqual(diff.patches[0].hunks.count, 1) + #expect(diff.patches[0].hunks.count == 1) let hunk = diff.patches[0].hunks[0] // Check the hunk lines - XCTAssertEqual(hunk.lines.count, 3) - XCTAssertEqual(hunk.lines[0].type, .deletion) - XCTAssertEqual(hunk.lines[0].content, "The index content!\n") + #expect(hunk.lines.count == 3) + #expect(hunk.lines[0].type == .deletion) + #expect(hunk.lines[0].content == "The index content!\n") - XCTAssertEqual(hunk.lines[1].content, "\n") + #expect(hunk.lines[1].content == "\n") - XCTAssertEqual(hunk.lines[2].type, .addition) - XCTAssertEqual(hunk.lines[2].content, "The working tree content!\n") + #expect(hunk.lines[2].type == .addition) + #expect(hunk.lines[2].content == "The working tree content!\n") } - func testDiffHEADToIndex() throws { - // Create a mock repository at the temporary directory - let repository = Repository.mock(named: "test-diff-head-index", in: Self.directory) + @Test("Diff HEAD to index") + func diffHEADToIndex() async throws { + let repository = mockRepository() // Create a base state for the test try createBaseStateForDiffHEAD(repository) @@ -87,24 +71,22 @@ final class RepositoryDiffTests: SwiftGitXTestCase { let diff = try repository.diff(to: .index) // Check if the diff count is correct - XCTAssertEqual(diff.patches[0].hunks.count, 1) + #expect(diff.patches[0].hunks.count == 1) let hunk = diff.patches[0].hunks[0] // Check the hunk lines - XCTAssertEqual(hunk.lines.count, 2) - XCTAssertEqual(hunk.lines[0].type, .deletion) - XCTAssertEqual(hunk.lines[0].content, "The commit content!\n") + #expect(hunk.lines.count == 2) + #expect(hunk.lines[0].type == .deletion) + #expect(hunk.lines[0].content == "The commit content!\n") - XCTAssertEqual(hunk.lines[1].type, .addition) - XCTAssertEqual(hunk.lines[1].content, "The index content!\n") + #expect(hunk.lines[1].type == .addition) + #expect(hunk.lines[1].content == "The index content!\n") } - // This method tests the created diff if the repository has a staged change and a working tree change. - // The staged change should be included in the diff. - func testDiffHEADToWorkingTreeWithIndex() throws { - // Create a mock repository at the temporary directory - let repository = Repository.mock(named: "test-diff-head-working-tree-index", in: Self.directory) + @Test("Diff HEAD to working tree with index") + func diffHEADToWorkingTreeWithIndex() async throws { + let repository = mockRepository() // Create a base state for the test try createBaseStateForDiffHEAD(repository) @@ -113,40 +95,45 @@ final class RepositoryDiffTests: SwiftGitXTestCase { let diff = try repository.diff(to: [.index, .workingTree]) // Check if the diff count is correct - XCTAssertEqual(diff.patches[0].hunks.count, 1) + #expect(diff.patches[0].hunks.count == 1) let hunk = diff.patches[0].hunks[0] // Check the hunk lines - XCTAssertEqual(hunk.lines.count, 3) - XCTAssertEqual(hunk.lines[0].type, .deletion) - XCTAssertEqual(hunk.lines[0].content, "The commit content!\n") + #expect(hunk.lines.count == 3) + #expect(hunk.lines[0].type == .deletion) + #expect(hunk.lines[0].content == "The commit content!\n") - XCTAssertEqual(hunk.lines[1].content, "\n") + #expect(hunk.lines[1].content == "\n") - XCTAssertEqual(hunk.lines[2].type, .addition) - XCTAssertEqual(hunk.lines[2].content, "The working tree content!\n") + #expect(hunk.lines[2].type == .addition) + #expect(hunk.lines[2].content == "The working tree content!\n") } - /// This method creates two commits in the repository and returns them. - private func mockCommits(repository: Repository) throws -> (initialCommit: Commit, secondCommit: Commit) { - let file = try repository.mockFile(named: "README.md", content: "Hello, SwiftGitX!\n") - - // Commit the changes - let initialCommit = try repository.mockCommit(message: "Initial commit", file: file) + /// This func creates base state for diff HEAD tests. It creates a commit, a staged change and a working tree change. + private func createBaseStateForDiffHEAD(_ repository: Repository) throws { + // Create a file + let file = try repository.mockFile(content: "The commit content!\n") - // Modify the file - try Data("Hello, World!\n".utf8).write(to: file) + // Create a commit + try repository.mockCommit(file: file) - // Commit the changes - let secondCommit = try repository.mockCommit(message: "Second commit", file: file) + // Update the file content and add the file + try Data("The index content!\n".utf8).write(to: file) + try repository.add(file: file) - return (initialCommit, secondCommit) + // Update the file content + try Data("\nThe working tree content!\n".utf8).write(to: file) } +} - func testDiffBetweenCommitAndCommit() throws { - // Create a mock repository at the temporary directory - let repository = Repository.mock(named: "test-diff-commit", in: Self.directory) +// MARK: - Diff Between Objects + +@Suite("Repository - Diff Between Objects", .tags(.repository, .operation, .diff)) +final class RepositoryDiffBetweenObjectsTests: SwiftGitXTest { + @Test("Diff between commit and commit") + func diffBetweenCommitAndCommit() async throws { + let repository = mockRepository() // Create commits let (initialCommit, secondCommit) = try mockCommits(repository: repository) @@ -155,27 +142,27 @@ final class RepositoryDiffTests: SwiftGitXTestCase { let diff = try repository.diff(from: initialCommit, to: secondCommit) // Check if the diff count is correct - XCTAssertEqual(diff.changes.count, 1) + #expect(diff.changes.count == 1) // Get the change - let change = try XCTUnwrap(diff.changes.first) + let change = try #require(diff.changes.first) // Check the change - XCTAssertEqual(change.oldFile.path, "README.md") - XCTAssertEqual(change.newFile.path, "README.md") - XCTAssertEqual(change.type, .modified) + #expect(change.oldFile.path == "README.md") + #expect(change.newFile.path == "README.md") + #expect(change.type == .modified) // Get the blob of the new file let newBlob: Blob = try repository.show(id: change.newFile.id) // Check the blob content - let newContent = try XCTUnwrap(String(data: newBlob.content, encoding: .utf8)) - XCTAssertEqual(newContent, "Hello, World!\n") + let newContent = try #require(String(data: newBlob.content, encoding: .utf8)) + #expect(newContent == "Hello, World!\n") } - func testDiffBetweenTreeAndTree() throws { - // Create a mock repository at the temporary directory - let repository = Repository.mock(named: "test-diff-tree", in: Self.directory) + @Test("Diff between tree and tree") + func diffBetweenTreeAndTree() async throws { + let repository = mockRepository() // Create commits let (initialCommit, secondCommit) = try mockCommits(repository: repository) @@ -184,27 +171,27 @@ final class RepositoryDiffTests: SwiftGitXTestCase { let diff = try repository.diff(from: initialCommit.tree, to: secondCommit.tree) // Check if the diff count is correct - XCTAssertEqual(diff.changes.count, 1) + #expect(diff.changes.count == 1) // Get the change - let change = try XCTUnwrap(diff.changes.first) + let change = try #require(diff.changes.first) // Check the change - XCTAssertEqual(change.oldFile.path, "README.md") - XCTAssertEqual(change.newFile.path, "README.md") - XCTAssertEqual(change.type, .modified) + #expect(change.oldFile.path == "README.md") + #expect(change.newFile.path == "README.md") + #expect(change.type == .modified) // Get the blob of the new file let newBlob: Blob = try repository.show(id: change.newFile.id) // Check the blob content - let newContent = try XCTUnwrap(String(data: newBlob.content, encoding: .utf8)) - XCTAssertEqual(newContent, "Hello, World!\n") + let newContent = try #require(String(data: newBlob.content, encoding: .utf8)) + #expect(newContent == "Hello, World!\n") } - func testDiffBetweenTagAndTag() throws { - // Create a mock repository at the temporary directory - let repository = Repository.mock(named: "test-diff-tag", in: Self.directory) + @Test("Diff between tag and tag") + func diffBetweenTagAndTag() async throws { + let repository = mockRepository() // Create commits let (initialCommit, secondCommit) = try mockCommits(repository: repository) @@ -219,27 +206,32 @@ final class RepositoryDiffTests: SwiftGitXTestCase { let diff = try repository.diff(from: initialTag, to: secondTag) // Check if the diff count is correct - XCTAssertEqual(diff.changes.count, 1) + #expect(diff.changes.count == 1) // Get the change - let change = try XCTUnwrap(diff.changes.first) + let change = try #require(diff.changes.first) // Check the change - XCTAssertEqual(change.oldFile.path, "README.md") - XCTAssertEqual(change.newFile.path, "README.md") - XCTAssertEqual(change.type, .modified) + #expect(change.oldFile.path == "README.md") + #expect(change.newFile.path == "README.md") + #expect(change.type == .modified) // Get the blob of the new file let newBlob: Blob = try repository.show(id: change.newFile.id) // Check the blob content - let newContent = try XCTUnwrap(String(data: newBlob.content, encoding: .utf8)) - XCTAssertEqual(newContent, "Hello, World!\n") + let newContent = try #require(String(data: newBlob.content, encoding: .utf8)) + #expect(newContent == "Hello, World!\n") } +} + +// MARK: - Diff Commit - func testDiffCommitParent() throws { - // Create a mock repository at the temporary directory - let repository = Repository.mock(named: "test-diff-commit-parent", in: Self.directory) +@Suite("Repository - Diff Commit", .tags(.repository, .operation, .diff)) +final class RepositoryDiffCommitTests: SwiftGitXTest { + @Test("Diff commit with parent") + func diffCommitParent() async throws { + let repository = mockRepository() // Create commits _ = try mockCommits(repository: repository) @@ -247,39 +239,39 @@ final class RepositoryDiffTests: SwiftGitXTestCase { // Remove old content and write new content than commit let headCommit = try repository.mockCommit( message: "Third commit", - file: repository.mockFile(named: "README.md", content: "Merhaba, Dünya!") + file: repository.mockFile(name: "README.md", content: "Merhaba, Dünya!") ) // Get the diff between the latest commit and its parent let diff = try repository.diff(commit: headCommit) // Check if the diff count is correct - XCTAssertEqual(diff.changes.count, 1) + #expect(diff.changes.count == 1) // Get the change - let change = try XCTUnwrap(diff.changes.first) + let change = try #require(diff.changes.first) // Check the change - XCTAssertEqual(change.type, .modified) - XCTAssertEqual(change.oldFile.path, "README.md") - XCTAssertEqual(change.newFile.path, "README.md") + #expect(change.type == .modified) + #expect(change.oldFile.path == "README.md") + #expect(change.newFile.path == "README.md") // Get the blob of the new file let newBlob: Blob = try repository.show(id: change.newFile.id) - let newText = try XCTUnwrap(String(data: newBlob.content, encoding: .utf8)) + let newText = try #require(String(data: newBlob.content, encoding: .utf8)) // Get the blob of the old file let oldBlob: Blob = try repository.show(id: change.oldFile.id) - let oldText = try XCTUnwrap(String(data: oldBlob.content, encoding: .utf8)) + let oldText = try #require(String(data: oldBlob.content, encoding: .utf8)) // Check the blob content and size - XCTAssertEqual(newText, "Merhaba, Dünya!") - XCTAssertEqual(oldText, "Hello, World!\n") + #expect(newText == "Merhaba, Dünya!") + #expect(oldText == "Hello, World!\n") } - func testDiffCommitNoParent() throws { - // Create a mock repository at the temporary directory - let repository = Repository.mock(named: "test-diff-commit-no-parent", in: Self.directory) + @Test("Diff commit with no parent") + func diffCommitNoParent() async throws { + let repository = mockRepository() // Create a commit let commit = try repository.mockCommit() @@ -288,107 +280,17 @@ final class RepositoryDiffTests: SwiftGitXTestCase { let diff = try repository.diff(commit: commit) // Check if the diff count is correct - XCTAssertEqual(diff.changes.count, 0) + #expect(diff.changes.count == 0) } +} - func testRepositoryStatusUntracked() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-status-untracked", in: Self.directory) - - // Create a new file in the repository - _ = try repository.mockFile(named: "README.md", content: "Hello, World!") - - // Get the status of the repository - let status = try repository.status() - - // Check the status of the repository - XCTAssertEqual(status.count, 1) - - // Get the status entry - let statusEntry = try XCTUnwrap(status.first) - - // Check the status entry properties - XCTAssertEqual(statusEntry.status, [.workingTreeNew]) - XCTAssertNil(statusEntry.index) // There is no index changes - - // Get working tree changes - let workingTreeChanges = try XCTUnwrap(statusEntry.workingTree) - - // Check the status entry diff delta properties - XCTAssertEqual(workingTreeChanges.type, .untracked) - - XCTAssertEqual(workingTreeChanges.newFile.path, "README.md") - XCTAssertEqual(workingTreeChanges.oldFile.path, "README.md") - - XCTAssertEqual(workingTreeChanges.newFile.size, "Hello, World!".count) - XCTAssertEqual(workingTreeChanges.oldFile.size, 0) - } - - func testRepositoryStatusAdded() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-status-added", in: Self.directory) - - // Create a new file in the repository - let file = try repository.mockFile(named: "README.md", content: "Hello, World!") - - // Add the file - try repository.add(path: file.lastPathComponent) - - // Get the status of the repository - let status = try repository.status() - - // Check the status of the repository - XCTAssertEqual(status.count, 1) - - // Get the status entry - let statusEntry = try XCTUnwrap(status.first) - - // Check the status entry properties - XCTAssertEqual(statusEntry.status, [.indexNew]) - XCTAssertNil(statusEntry.workingTree) // There is no working tree changes - let statusEntryDiffDelta = try XCTUnwrap(statusEntry.index) - - // Check the status entry diff delta properties - XCTAssertEqual(statusEntryDiffDelta.type, .added) - - XCTAssertEqual(statusEntryDiffDelta.newFile.path, "README.md") - XCTAssertEqual(statusEntryDiffDelta.oldFile.path, "README.md") - - XCTAssertEqual(statusEntryDiffDelta.newFile.size, "Hello, World!".count) - XCTAssertEqual(statusEntryDiffDelta.oldFile.size, 0) - - // Get the blob of the new file - let blob: Blob = try repository.show(id: statusEntryDiffDelta.newFile.id) - let blobText = try XCTUnwrap(String(data: blob.content, encoding: .utf8)) - XCTAssertEqual(blobText, "Hello, World!") - } - - func testRepositoryStatusFile_NewAndModified() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-status-new-and-modified", in: Self.directory) - - // Create a new file in the repository - let file = try repository.mockFile(named: "README.md", content: "Hello, World!") - - // Add the file - try repository.add(file: file) - - // Modify the file - try Data("Merhaba, Dünya!".utf8).write(to: file) - - // Get the status of the repository - let status: [StatusEntry.Status] = try repository.status(file: file) - - // Check the status of the repository - XCTAssertEqual(status.count, 2) +// MARK: - Diff Equality - // Check the status entry properties - XCTAssertEqual(status, [.indexNew, .workingTreeModified]) - } - - func testDiffEquality() throws { - // Create a repository at the directory - let repository = Repository.mock(named: "test-diff-equality", in: Self.directory) +@Suite("Repository - Diff Equality", .tags(.repository, .operation, .diff)) +final class RepositoryDiffEqualityTests: SwiftGitXTest { + @Test("Diff equality") + func diffEquality() async throws { + let repository = mockRepository() // Create mock commits let (initialCommit, secondCommit) = try mockCommits(repository: repository) @@ -403,149 +305,24 @@ final class RepositoryDiffTests: SwiftGitXTestCase { let sameDiff = try sameRepository.diff(from: initialCommit, to: secondCommit) // Check the diff properties are equal between the two repositories - XCTAssertEqual(sameDiff, diff) - } - - func testPatchCreateFromBlobs() throws { - // Create a repository at the directory - let repository = Repository.mock(named: "test-patch-create-from-blobs", in: Self.directory) - - // Create a commit - let file = try repository.mockFile(named: "README.md", content: "The old data!\n") - try repository.mockCommit(file: file) - - // Update the file content and add the file - try Data("The new data!\n".utf8).write(to: file) - // * The working tree file does not have a blob object, so we need to add the file at least. - try repository.add(file: file) - - // Get the status of the file - let status = try XCTUnwrap(repository.status().first) - XCTAssertEqual(status.status, [.indexModified]) - - // Lookup blobs - let oldBlobID = try XCTUnwrap(status.index?.oldFile.id) - let oldBlob: Blob = try XCTUnwrap(repository.show(id: oldBlobID)) - - let newBlobID = try XCTUnwrap(status.index?.newFile.id) - let newBlob: Blob = try XCTUnwrap(repository.show(id: newBlobID)) - - // Create patch from status blobs - let patch = try repository.patch(from: oldBlob, to: newBlob) - - // Check the patch properties - XCTAssertEqual(patch.hunks.count, 1) - XCTAssertEqual(patch.hunks[0].lines[0].content, "The old data!\n") - XCTAssertEqual(patch.hunks[0].lines[1].content, "The new data!\n") + #expect(sameDiff == diff) } +} - func testPatchCreateFromBlobToFile() throws { - // Create a repository at the directory - let repository = Repository.mock(named: "test-patch-create-from-blob-to-file", in: Self.directory) - - // Create a commit - let file = try repository.mockFile(named: "README.md", content: "The old data!\n") - try repository.mockCommit(file: file) - - // Update the file content and add the file - try Data("The new data!\n".utf8).write(to: file) - - // Get the status of the file - let status = try XCTUnwrap(repository.status().first) - XCTAssertEqual(status.status, [.workingTreeModified]) - - // Lookup blobs - let oldBlobID = try XCTUnwrap(status.workingTree?.oldFile.id) - let oldBlob: Blob = try XCTUnwrap(repository.show(id: oldBlobID)) - - // Create patch from status blobs - let patch = try repository.patch(from: oldBlob, to: file) - - // Check the patch properties - XCTAssertEqual(patch.hunks.count, 1) - XCTAssertEqual(patch.hunks[0].lines[0].content, "The old data!\n") - XCTAssertEqual(patch.hunks[0].lines[1].content, "The new data!\n") - } - - func testPatchCreateFromDelta_Modified() throws { - // Create a repository at the directory - let repository = Repository.mock(named: "test-patch-create-from-delta--modified", in: Self.directory) - - // Create a commit - let file = try repository.mockFile(named: "README.md", content: "The old data!\n") - try repository.mockCommit(file: file) - - // Update the file content and add the file - try Data("The new data!\n".utf8).write(to: file) - - // Get the status of the file - let status: StatusEntry = try XCTUnwrap(repository.status().first) - XCTAssertEqual(status.status, [.workingTreeModified]) - let workingTreeDelta = try XCTUnwrap(status.workingTree) - - // Create patch from workingTree delta - let workingTreePatch = try XCTUnwrap(repository.patch(from: workingTreeDelta)) - - // Check the patch properties - XCTAssertEqual(workingTreePatch.hunks.count, 1) - XCTAssertEqual(workingTreePatch.hunks[0].lines[0].content, "The old data!\n") - XCTAssertEqual(workingTreePatch.hunks[0].lines[1].content, "The new data!\n") - } - - func testPatchCreateFromDelta_Indexed() throws { - // Create a repository at the directory - let repository = Repository.mock(named: "test-patch-create-from-delta--indexed", in: Self.directory) - - // Create a commit - let file = try repository.mockFile(named: "README.md", content: "The old data!\n") - try repository.mockCommit(file: file) - - // Update the file content and add the file - try Data("The new data!\n".utf8).write(to: file) - try repository.add(file: file) - - // Get the status of the file - let status: StatusEntry = try XCTUnwrap(repository.status().first) - XCTAssertEqual(status.status, [.indexModified]) - let indexDelta = try XCTUnwrap(status.index) - - // Create patch from workingTree delta - let indexPatch = try XCTUnwrap(repository.patch(from: indexDelta)) - - // Check the patch properties - XCTAssertEqual(indexPatch.hunks.count, 1) - XCTAssertEqual(indexPatch.hunks[0].lines[0].content, "The old data!\n") - XCTAssertEqual(indexPatch.hunks[0].lines[1].content, "The new data!\n") - } - - func testPatchCreateFromDelta_Untracked() throws { - // Create a repository at the directory - let repository = Repository.mock(named: "test-patch-create-from-delta--untracked", in: Self.directory) - - // Create a new file in the repository - _ = try repository.mockFile(named: "README.md", content: "Hello, World!\n") - - // Get the status of the file - let status: StatusEntry = try XCTUnwrap(repository.status().first) - XCTAssertEqual(status.status, [.workingTreeNew]) // The file is untracked - let workingTreeDelta = try XCTUnwrap(status.workingTree) +// MARK: - Helper Functions - // Create patch from workingTree delta - let workingTreePatch = try XCTUnwrap(repository.patch(from: workingTreeDelta)) +/// This method creates two commits in the repository and returns them. +private func mockCommits(repository: Repository) throws -> (initialCommit: Commit, secondCommit: Commit) { + let file = try repository.mockFile(name: "README.md", content: "Hello, SwiftGitX!\n") - // Check the patch properties - XCTAssertEqual(workingTreePatch.hunks.count, 1) - XCTAssertEqual(workingTreePatch.hunks[0].lines[0].content, "Hello, World!\n") - } + // Commit the changes + let initialCommit = try repository.mockCommit(message: "Initial commit", file: file) - func testPatchCreateEmptyBlobs() throws { - // Create a repository at the directory - let repository = Repository.mock(named: "test-patch-create-empty-blobs", in: Self.directory) + // Modify the file + try Data("Hello, World!\n".utf8).write(to: file) - // Create patch from empty blobs - let patch = try repository.patch(from: nil, to: nil) + // Commit the changes + let secondCommit = try repository.mockCommit(message: "Second commit", file: file) - // Check the patch properties - XCTAssertEqual(patch.hunks.count, 0) - } + return (initialCommit, secondCommit) } diff --git a/Tests/SwiftGitXTests/RepositoryTests/RepositoryFetchTests.swift b/Tests/SwiftGitXTests/RepositoryTests/RepositoryFetchTests.swift new file mode 100644 index 0000000..36f1bf4 --- /dev/null +++ b/Tests/SwiftGitXTests/RepositoryTests/RepositoryFetchTests.swift @@ -0,0 +1,28 @@ +import Foundation +import SwiftGitX +import Testing + +@Suite("Repository - Fetch", .tags(.repository, .operation, .fetch)) +final class RepositoryFetchTests: SwiftGitXTest { + @Test("Fetch from remote repository") + func fetchFromRemote() async throws { + // Create remote repository + let remoteRepository = mockRepository(suffix: "--remote") + + // Create mock commit in the remote repository + try remoteRepository.mockCommit() + + // Create local repository + let localRepository = mockRepository(suffix: "--local") + + // Add remote repository to the local repository + try localRepository.remote.add(named: "origin", at: remoteRepository.workingDirectory) + + // Fetch the commit from the remote repository + try await localRepository.fetch() + + // Check if the remote branch is fetched + let remoteBranch = try localRepository.branch.get(named: "origin/main") + #expect(try remoteBranch.target.id == remoteRepository.HEAD.target.id) + } +} diff --git a/Tests/SwiftGitXTests/RepositoryTests/RepositoryLogTests.swift b/Tests/SwiftGitXTests/RepositoryTests/RepositoryLogTests.swift new file mode 100644 index 0000000..4cafcb0 --- /dev/null +++ b/Tests/SwiftGitXTests/RepositoryTests/RepositoryLogTests.swift @@ -0,0 +1,18 @@ +import SwiftGitX +import Testing + +@Suite("Repository - Log", .tags(.repository, .operation, .log)) +final class RepositoryLogTests: SwiftGitXTest { + @Test("Log returns commits in order") + func log() async throws { + let repository = mockRepository() + + // Create multiple commits + let commits = try (0..<10).map { _ in try repository.mockCommit() } + + // Get log with reverse sorting + let commitSequence = try repository.log(from: repository.HEAD, sorting: .reverse) + let logCommits = Array(commitSequence) + #expect(logCommits == commits) + } +} diff --git a/Tests/SwiftGitXTests/RepositoryTests/RepositoryOperationTests.swift b/Tests/SwiftGitXTests/RepositoryTests/RepositoryOperationTests.swift deleted file mode 100644 index 03cef2b..0000000 --- a/Tests/SwiftGitXTests/RepositoryTests/RepositoryOperationTests.swift +++ /dev/null @@ -1,274 +0,0 @@ -// -// RepositoryOperationTests.swift -// -// -// Created by İbrahim Çetin on 18.06.2024. -// - -import SwiftGitX -import XCTest - -final class RepositoryOperationTests: SwiftGitXTestCase { - func testAdd() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-add", in: Self.directory) - - // Create a new file in the repository - let file = try repository.mockFile(named: "README.md") - - // Add the file to the index - try repository.add(file: file) - - // Get status of the repository - let status = try repository.status(file: file) - - // Check if the file is added to the index - XCTAssertEqual(status, [.indexNew]) - } - - func testCommit() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-commit", in: Self.directory) - - // Create a new file in the repository - let file = try repository.mockFile(named: "README.md") - - // Add the file to the index - try repository.add(file: file) - - // Commit the changes - let commit = try repository.commit(message: "Initial commit") - - // Get the HEAD commit - let headCommit = try XCTUnwrap(repository.HEAD.target as? Commit) - - // Check if the HEAD commit is the same as the created commit - XCTAssertEqual(commit, headCommit) - } - - func testEmptyCommit() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-empty-commit", in: Self.directory) - - // Create initial commit - try repository.mockCommit(message: "Initial commit") - - // Verify that committing with no changes fails by default - XCTAssertThrowsError(try repository.commit(message: "Empty commit without option")) { error in - XCTAssertTrue(error is SwiftGitXError) - let error = error as? SwiftGitXError - - XCTAssertEqual(error?.code, .unchanged) - XCTAssertEqual(error?.category, .repository) - XCTAssertEqual(error?.message, "no changes are staged for commit") - } - - // Verify that committing with allowEmpty option succeeds - let emptyCommit = try repository.commit(message: "Empty commit with option", options: .allowEmpty) - - // Get the HEAD commit - let headCommit = try XCTUnwrap(repository.HEAD.target as? Commit) - - // Check if the HEAD commit is the same as the created empty commit - XCTAssertEqual(emptyCommit, headCommit) - - // Verify the commit message - XCTAssertEqual(emptyCommit.message, "Empty commit with option") - } - - func testReset() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-reset", in: Self.directory) - - let initialCommit = try repository.mockCommit() - - // Create a new file in the repository - let file = try repository.mockFile(named: "ResetMe.md") - - // Add the file to the index - try repository.add(file: file) - - // Reset the staged changes - try repository.reset(from: initialCommit, files: [file]) - - // Get the status of the file - let status = try repository.status(file: file) - - // Check if the file is reset - XCTAssertEqual(status, [.workingTreeNew]) - } - - func testResetSoft() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-reset-soft", in: Self.directory) - - // Create mock commit - let initialCommit = try repository.mockCommit() - - // Create a oops commit - try repository.mockCommit( - message: "Oops!", - file: repository.mockFile(named: "Undefined", content: "Reset me!") - ) - - // Reset the repository to the previous commit - try repository.reset(to: initialCommit) - - // Get the HEAD commit - let headCommit = try XCTUnwrap(repository.HEAD.target as? Commit) - - // Check if the HEAD commit is the same as the previous commit - XCTAssertEqual(headCommit, initialCommit) - } - - func testRestoreWorkingTree() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-restore-working-tree", in: Self.directory) - - // Create a new file - let fileToRestore = try repository.mockFile(named: "WorkingTree.md", content: "Hello, World!") - - // Commit the file - try repository.mockCommit(message: "Initial commit", file: fileToRestore) - - // Modify the file - try Data("Restore me!".utf8).write(to: fileToRestore) - - // Create a new file to stage (this should not be restored) - let fileToStage = try repository.mockFile(named: "Stage.md", content: "Stage me!") - - // Stage the file - try repository.add(file: fileToStage) - - // Restore the file to the head commit - try repository.restore(paths: ["WorkingTree.md", "Stage.md"]) - - // Check if the file content is the same as the head commit - let restoredFileContent = try String(contentsOf: fileToRestore) - - XCTAssertEqual(restoredFileContent, "Hello, World!") - XCTAssertTrue(FileManager.default.fileExists(atPath: fileToStage.path)) - } - - func testRestoreStage() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-restore-stage", in: Self.directory) - - // Create a new file - let workingTreeFile = try repository.mockFile(named: "WorkingTree.md", content: "Hello, World!") - - // Commit the file - try repository.mockCommit(message: "Initial commit", file: workingTreeFile) - - // Modify the file (this should not be restored) - try Data("Should not be restored!".utf8).write(to: workingTreeFile) - - // Create a new file to stage - let stagedFile = try repository.mockFile(named: "Stage.md", content: "Stage me!") - - // Stage the file - try repository.add(file: stagedFile) - - // Restore the staged file - try repository.restore(.staged, paths: ["WorkingTree.md", "Stage.md"]) - - // Check the status of the staged file and content - let stagedFileStatus = try repository.status(file: stagedFile) - XCTAssertEqual(stagedFileStatus, [.workingTreeNew]) - XCTAssertEqual(try String(contentsOf: stagedFile), "Stage me!") - - // Check the status of the working tree file and content - let workingTreeFileStatus = try repository.status(file: workingTreeFile) - XCTAssertEqual(workingTreeFileStatus, [.workingTreeModified]) - XCTAssertTrue(FileManager.default.fileExists(atPath: workingTreeFile.path)) - XCTAssertEqual(try String(contentsOf: workingTreeFile), "Should not be restored!") - } - - func testRestoreWorkingTreeAndStage() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-restore-working-tree-stage", in: Self.directory) - - // Create a mock commit - try repository.mockCommit() - - // Modify the file which is created in mockCommit - let file = try repository.mockFile(named: "README.md", content: "Restore stage area!") - - // Add the file to the index - try repository.add(file: file) - - // Modify the file - try Data("Restore working tree!".utf8).write(to: file) - - // Restore the working tree and stage - try repository.restore([.workingTree, .staged], files: [file]) - - // Check the status of the file and the content - let stagedFileStatus = try repository.status(file: file) - XCTAssertTrue(stagedFileStatus.isEmpty) // There should be no changes (all changes are restored) - XCTAssertEqual(try String(contentsOf: file), "Welcome to SwiftGitX!\n") - - // Create a new file to delete (this should be deleted) - let fileToDelete = try repository.mockFile(named: "DeleteMe.md", content: "Delete me from stage area!") - - // Add the file to the index - try repository.add(file: fileToDelete) - - // Modify the file - try Data("Delete me from working tree!".utf8).write(to: fileToDelete) - - // Restore the working tree and stage - try repository.restore([.workingTree, .staged], files: [fileToDelete]) - - // File should be deleted - XCTAssertFalse(FileManager.default.fileExists(atPath: fileToDelete.path)) - } - - func testRepositoryLog() async throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-log", in: Self.directory) - - var createdCommits = [Commit]() - for index in 0..<10 { - // Create a commit - let commit = try repository.mockCommit( - message: "Commit \(index)", - file: repository.mockFile(named: "README-\(index).md") - ) - - createdCommits.append(commit) - } - - // Get the log of the repository - let commitSequence = try repository.log(from: repository.HEAD, sorting: .reverse) - let logCommits = Array(commitSequence) - - // Check if the commits are the same - XCTAssertEqual(logCommits, createdCommits) - } - - func testRevert() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-revert", in: Self.directory) - - let file = try repository.mockFile(named: "README.md", content: "Hello, World!") - - // Create initial commit - try repository.mockCommit(message: "Initial commit", file: file) - - // Modify the file - try Data("Revert me!".utf8).write(to: file) - - // Create a new commit - let commitToRevert = try repository.mockCommit(message: "Second commit", file: file) - - // Revert the commit - try repository.revert(commitToRevert) - - // Check the status of the file - XCTAssertEqual(try repository.status(file: file), [.indexModified]) - - // Check the content of the file - XCTAssertEqual(try String(contentsOf: file), "Hello, World!") - } -} diff --git a/Tests/SwiftGitXTests/RepositoryTests/RepositoryPatchTests.swift b/Tests/SwiftGitXTests/RepositoryTests/RepositoryPatchTests.swift new file mode 100644 index 0000000..a60282a --- /dev/null +++ b/Tests/SwiftGitXTests/RepositoryTests/RepositoryPatchTests.swift @@ -0,0 +1,149 @@ +import Foundation +import SwiftGitX +import Testing + +@Suite("Repository - Patch", .tags(.repository, .operation, .patch)) +final class RepositoryPatchTests: SwiftGitXTest { + @Test("Patch create from blobs") + func patchCreateFromBlobs() async throws { + let repository = mockRepository() + + // Create a commit + let file = try repository.mockFile(content: "The old data!\n") + try repository.mockCommit(file: file) + + // Update the file content and add the file + try Data("The new data!\n".utf8).write(to: file) + // * The working tree file does not have a blob object, so we need to add the file at least. + try repository.add(file: file) + + // Get the status of the file + let status = try #require(repository.status().first) + #expect(status.status == [.indexModified]) + + // Lookup blobs + let oldBlobID = try #require(status.index?.oldFile.id) + let oldBlob: Blob = try repository.show(id: oldBlobID) + + let newBlobID = try #require(status.index?.newFile.id) + let newBlob: Blob = try repository.show(id: newBlobID) + + // Create patch from status blobs + let patch = try repository.patch(from: oldBlob, to: newBlob) + + // Check the patch properties + #expect(patch.hunks.count == 1) + #expect(patch.hunks[0].lines[0].content == "The old data!\n") + #expect(patch.hunks[0].lines[1].content == "The new data!\n") + } + + @Test("Patch create from blob to file") + func patchCreateFromBlobToFile() async throws { + let repository = mockRepository() + + // Create a commit + let file = try repository.mockFile(content: "The old data!\n") + try repository.mockCommit(file: file) + + // Update the file content and add the file + try Data("The new data!\n".utf8).write(to: file) + + // Get the status of the file + let status = try #require(repository.status().first) + #expect(status.status == [.workingTreeModified]) + + // Lookup blobs + let oldBlobID = try #require(status.workingTree?.oldFile.id) + let oldBlob: Blob = try repository.show(id: oldBlobID) + + // Create patch from status blobs + let patch = try repository.patch(from: oldBlob, to: file) + + // Check the patch properties + #expect(patch.hunks.count == 1) + #expect(patch.hunks[0].lines[0].content == "The old data!\n") + #expect(patch.hunks[0].lines[1].content == "The new data!\n") + } + + @Test("Patch create from delta modified") + func patchCreateFromDeltaModified() async throws { + let repository = mockRepository() + + // Create a commit + let file = try repository.mockFile(content: "The old data!\n") + try repository.mockCommit(file: file) + + // Update the file content and add the file + try Data("The new data!\n".utf8).write(to: file) + + // Get the status of the file + let status: StatusEntry = try #require(repository.status().first) + #expect(status.status == [.workingTreeModified]) + let workingTreeDelta = try #require(status.workingTree) + + // Create patch from workingTree delta + let workingTreePatch = try #require((try repository.patch(from: workingTreeDelta))) + + // Check the patch properties + #expect(workingTreePatch.hunks.count == 1) + #expect(workingTreePatch.hunks[0].lines[0].content == "The old data!\n") + #expect(workingTreePatch.hunks[0].lines[1].content == "The new data!\n") + } + + @Test("Patch create from delta indexed") + func patchCreateFromDeltaIndexed() async throws { + let repository = mockRepository() + + // Create a commit + let file = try repository.mockFile(content: "The old data!\n") + try repository.mockCommit(file: file) + + // Update the file content and add the file + try Data("The new data!\n".utf8).write(to: file) + try repository.add(file: file) + + // Get the status of the file + let status: StatusEntry = try #require(repository.status().first) + #expect(status.status == [.indexModified]) + let indexDelta = try #require(status.index) + + // Create patch from workingTree delta + let indexPatch = try #require((try repository.patch(from: indexDelta))) + + // Check the patch properties + #expect(indexPatch.hunks.count == 1) + #expect(indexPatch.hunks[0].lines[0].content == "The old data!\n") + #expect(indexPatch.hunks[0].lines[1].content == "The new data!\n") + } + + @Test("Patch create from delta untracked") + func patchCreateFromDeltaUntracked() async throws { + let repository = mockRepository() + + // Create a new file in the repository + _ = try repository.mockFile(content: "Hello, World!\n") + + // Get the status of the file + let status: StatusEntry = try #require(repository.status().first) + #expect(status.status == [.workingTreeNew]) // The file is untracked + let workingTreeDelta = try #require(status.workingTree) + + // Create patch from workingTree delta + let workingTreePatch = try #require((try repository.patch(from: workingTreeDelta))) + + // Check the patch properties + #expect(workingTreePatch.hunks.count == 1) + #expect(workingTreePatch.hunks[0].lines[0].content == "Hello, World!\n") + } + + @Test("Patch create from empty blobs") + func patchCreateFromEmptyBlobs() async throws { + let repository = mockRepository() + + // Create patch from empty blobs + let patch = try repository.patch(from: nil, to: nil) + + // Check the patch properties + #expect(patch.hunks.count == 0) + } +} diff --git a/Tests/SwiftGitXTests/RepositoryTests/RepositoryPropertyTests.swift b/Tests/SwiftGitXTests/RepositoryTests/RepositoryPropertyTests.swift index 5f9e308..a9aa671 100644 --- a/Tests/SwiftGitXTests/RepositoryTests/RepositoryPropertyTests.swift +++ b/Tests/SwiftGitXTests/RepositoryTests/RepositoryPropertyTests.swift @@ -1,10 +1,12 @@ +import Foundation import SwiftGitX -import XCTest +import Testing -final class RepositoryPropertyTests: SwiftGitXTestCase { - func testRepositoryHEAD() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-head", in: Self.directory) +@Suite("Repository - Properties", .tags(.repository)) +final class RepositoryPropertyTests: SwiftGitXTest { + @Test("Repository HEAD") + func repositoryHEAD() async throws { + let repository = mockRepository() // Commit the file try repository.mockCommit() @@ -13,82 +15,96 @@ final class RepositoryPropertyTests: SwiftGitXTestCase { let head = try repository.HEAD // Check the HEAD reference - XCTAssertEqual(head.name, "main") - XCTAssertEqual(head.fullName, "refs/heads/main") + #expect(head.name == "main") + #expect(head.fullName == "refs/heads/main") } - func testRepositoryHEADUnborn() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-head-unborn", in: Self.directory) + @Test("Repository HEAD unborn") + func repositoryHEADUnborn() async throws { + let repository = mockRepository() - XCTAssertTrue(repository.isHEADUnborn) + #expect(repository.isHEADUnborn) - XCTAssertThrowsError(try repository.HEAD) + let error = #expect(throws: SwiftGitXError.self) { + try repository.HEAD + } + + #expect(error?.code == .unbornBranch) + #expect(error?.category == .reference) + #expect(error?.message == "reference 'refs/heads/main' not found") } - func testRepositoryWorkingDirectory() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-working-directory", in: Self.directory) + @Test("Repository working directory") + func repositoryWorkingDirectory() async throws { + let repository = mockRepository() // Get the working directory of the repository - let repositoryWorkingDirectory = try XCTUnwrap(repository.workingDirectory) - - // Get the path of the mock repository directory - let expectedDirectory = - if Self.directory.isEmpty { - URL.temporaryDirectory.appending(components: "SwiftGitXTests", "test-working-directory/") - } else { - URL.temporaryDirectory.appending( - components: "SwiftGitXTests", Self.directory, "test-working-directory/") - } - - // Check if the working directory is the same as the expected directory - XCTAssertEqual(repositoryWorkingDirectory.resolvingSymlinksInPath(), expectedDirectory) + let repositoryWorkingDirectory = try repository.workingDirectory + + // The working directory should exist and be valid + #expect(repositoryWorkingDirectory.hasDirectoryPath) + #expect(repositoryWorkingDirectory.lastPathComponent != ".git") + + // Expected path for the repository working directory + let expectedWorkingDirectory = URL.temporaryDirectory + .appending(component: "SwiftGitXTests") + .appending(components: "RepositoryPropertyTests", "RepositoryPropertyTests", "repositoryWorkingDirectory/") + + #expect(repositoryWorkingDirectory.resolvingSymlinksInPath() == expectedWorkingDirectory) } - func testRepositoryPath() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-path", in: Self.directory) + @Test("Repository path") + func repositoryPath() async throws { + let repository = mockRepository() + + // The repository path should point to the .git directory + #expect(repository.path.lastPathComponent == ".git") + #expect(repository.path.hasDirectoryPath) - // Get the path of the mock repository directory - let expectedDirectory = - if Self.directory.isEmpty { - URL.temporaryDirectory.appending(components: "SwiftGitXTests", "test-path/.git/") - } else { - URL.temporaryDirectory.appending(components: "SwiftGitXTests", Self.directory, "test-path/.git/") - } + // Expected path for the repository working directory + let expectedPath = URL.temporaryDirectory + .appending(component: "SwiftGitXTests") + .appending(components: "RepositoryPropertyTests", "RepositoryPropertyTests", "repositoryPath", ".git/") - // Check if the path is the same as the expected directory - XCTAssertEqual(repository.path.resolvingSymlinksInPath(), expectedDirectory) + #expect(repository.path.resolvingSymlinksInPath() == expectedPath) } - func testRepositoryPath_Bare() { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-path-bare", in: Self.directory, isBare: true) + @Test("Repository path for bare repository") + func repositoryPathBare() async throws { + let repository = mockRepository(isBare: true) - // Get the path of the mock repository directory - let expectedDirectory = - if Self.directory.isEmpty { - URL.temporaryDirectory.appending(components: "SwiftGitXTests", "test-path-bare/") - } else { - URL.temporaryDirectory.appending(components: "SwiftGitXTests", Self.directory, "test-path-bare/") - } + // For bare repositories, the path should not end with .git + #expect(repository.path.lastPathComponent != ".git") + #expect(repository.path.hasDirectoryPath) - // Check if the path is the same as the expected directory - XCTAssertEqual(repository.path.resolvingSymlinksInPath(), expectedDirectory) + // Expected path for the repository path + let expectedPath = URL.temporaryDirectory + .appending(component: "SwiftGitXTests") + .appending(components: "RepositoryPropertyTests", "RepositoryPropertyTests", "repositoryPathBare/") + + #expect(repository.path.resolvingSymlinksInPath() == expectedPath) + + // Bare repositories don't have a working directory + let error = #expect(throws: SwiftGitXError.self) { + try repository.workingDirectory + } + + #expect(error?.code == .error) + #expect(error?.category == .repository) + #expect(error?.message == "Failed to get working directory") } - func testRepositoryIsEmpty() throws { - // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-is-empty", in: Self.directory) + @Test("Repository is empty") + func repositoryIsEmpty() async throws { + let repository = mockRepository() // Check if the repository is empty - XCTAssertTrue(repository.isEmpty) + #expect(repository.isEmpty) // Create a commit - _ = try repository.mockCommit() + try repository.mockCommit() // Check if the repository is not empty - XCTAssertFalse(repository.isEmpty) + #expect(repository.isEmpty == false) } } diff --git a/Tests/SwiftGitXTests/RepositoryTests/RepositoryPushTests.swift b/Tests/SwiftGitXTests/RepositoryTests/RepositoryPushTests.swift new file mode 100644 index 0000000..fe36d50 --- /dev/null +++ b/Tests/SwiftGitXTests/RepositoryTests/RepositoryPushTests.swift @@ -0,0 +1,60 @@ +import Foundation +import SwiftGitX +import Testing + +@Suite("Repository - Push", .tags(.repository, .operation, .push)) +final class RepositoryPushTests: SwiftGitXTest { + @Test("Push to remote repository") + func pushToRemote() async throws { + // Create a mock repository at the temporary directory + let source = URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! + let remoteDirectory = mockDirectory(suffix: "--remote") + let remoteRepository = try await Repository.clone(from: source, to: remoteDirectory, options: .bare) + + // Clone the remote repository to the local repository + let localDirectory = mockDirectory(suffix: "--local") + let localRepository = try await Repository.clone(from: remoteDirectory, to: localDirectory) + + // Create a new commit in the local repository + try localRepository.mockCommit(message: "Pushed commit", file: localRepository.mockFile(name: "PushedFile.md")) + + // Push the commit to the remote repository + try await localRepository.push() + + // Check if the commit is pushed + #expect(try localRepository.HEAD.target.id == remoteRepository.HEAD.target.id) + } + + @Test("Push to empty remote and set upstream") + func pushEmptyRemoteSetUpstream() async throws { + // Create a mock repository at the temporary directory + let remoteRepository = mockRepository(suffix: "--remote", isBare: true) + + // Create a mock repository at the temporary directory + let localRepository = mockRepository(suffix: "--local") + + // Create a new commit in the local repository + try localRepository.mockCommit(message: "Pushed commit", file: localRepository.mockFile(name: "PushedFile.md")) + + // Add remote repository to the local repository + try localRepository.remote.add(named: "origin", at: remoteRepository.path) + + // Push the commit to the remote repository + try await localRepository.push() + + // Check if the commit is pushed + #expect(try localRepository.HEAD.target.id == remoteRepository.HEAD.target.id) + + // Upstream branch should be nil + #expect(try localRepository.branch.current.upstream == nil) + + // Set the upstream branch + try localRepository.branch.setUpstream(to: localRepository.branch.get(named: "origin/main")) + + // Check if the upstream branch is set + let upstreamBranch = try #require(localRepository.branch.current.upstream as? Branch) + #expect(upstreamBranch.target.id == (try remoteRepository.HEAD.target.id)) + #expect(upstreamBranch.name == "origin/main") + #expect(upstreamBranch.fullName == "refs/remotes/origin/main") + } +} diff --git a/Tests/SwiftGitXTests/RepositoryTests/RepositoryRemoteOperationTests.swift b/Tests/SwiftGitXTests/RepositoryTests/RepositoryRemoteOperationTests.swift deleted file mode 100644 index 6c9d0bf..0000000 --- a/Tests/SwiftGitXTests/RepositoryTests/RepositoryRemoteOperationTests.swift +++ /dev/null @@ -1,77 +0,0 @@ -import SwiftGitX -import XCTest - -final class RepositoryRemoteOperationTests: SwiftGitXTestCase { - func testRepositoryPush() async throws { - // Create a mock repository at the temporary directory - let source = URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! - let remoteDirectory = Repository.mockDirectory(named: "test-push--remote", in: Self.directory) - let remoteRepository = try await Repository.clone(from: source, to: remoteDirectory, options: .bare) - - // Clone the remote repository to the local repository - let localDirectory = Repository.mockDirectory(named: "test-push--local", in: Self.directory) - let localRepository = try await Repository.clone(from: remoteDirectory, to: localDirectory) - - // Create a new commit in the local repository - try localRepository.mockCommit(message: "Pushed commit", file: localRepository.mockFile(named: "PushedFile.md")) - - // Push the commit to the remote repository - try await localRepository.push() - - // Check if the commit is pushed - try XCTAssertEqual(localRepository.HEAD.target.id, remoteRepository.HEAD.target.id) - } - - func testRepositoryPushEmptyRemote_SetUpstream() async throws { - // Create a mock repository at the temporary directory - let remoteRepository = Repository.mock(named: "test-push-empty--remote", in: Self.directory, isBare: true) - - // Create a mock repository at the temporary directory - let localRepository = Repository.mock(named: "test-push-empty--local", in: Self.directory) - - // Create a new commit in the local repository - try localRepository.mockCommit(message: "Pushed commit", file: localRepository.mockFile(named: "PushedFile.md")) - - // Add remote repository to the local repository - try localRepository.remote.add(named: "origin", at: remoteRepository.path) - - // Push the commit to the remote repository - try await localRepository.push() - - // Check if the commit is pushed - try XCTAssertEqual(localRepository.HEAD.target.id, remoteRepository.HEAD.target.id) - - // Upstream branch should be nil - try XCTAssertNil(localRepository.branch.current.upstream) - - // Set the upstream branch - try localRepository.branch.setUpstream(to: localRepository.branch.get(named: "origin/main")) - - // Check if the upstream branch is set - let upstreamBranch = try XCTUnwrap(localRepository.branch.current.upstream as? Branch) - XCTAssertEqual(upstreamBranch.target.id, try remoteRepository.HEAD.target.id) - XCTAssertEqual(upstreamBranch.name, "origin/main") - XCTAssertEqual(upstreamBranch.fullName, "refs/remotes/origin/main") - } - - func testRepositoryFetch() async throws { - // Create remote repository - let remoteRepository = Repository.mock(named: "test-fetch--remote", in: Self.directory) - - // Create mock commit in the remote repository - try remoteRepository.mockCommit() - - // Create local repository - let localRepository = Repository.mock(named: "test-fetch--local", in: Self.directory) - - // Add remote repository to the local repository - try localRepository.remote.add(named: "origin", at: remoteRepository.workingDirectory) - - // Fetch the commit from the remote repository - try await localRepository.fetch() - - // Check if the remote branch is fetched - let remoteBranch = try localRepository.branch.get(named: "origin/main") - try XCTAssertEqual(remoteBranch.target.id, remoteRepository.HEAD.target.id) - } -} diff --git a/Tests/SwiftGitXTests/RepositoryTests/RepositoryResetTests.swift b/Tests/SwiftGitXTests/RepositoryTests/RepositoryResetTests.swift new file mode 100644 index 0000000..826a13c --- /dev/null +++ b/Tests/SwiftGitXTests/RepositoryTests/RepositoryResetTests.swift @@ -0,0 +1,40 @@ +import SwiftGitX +import Testing + +@Suite("Repository - Reset", .tags(.repository, .operation, .reset)) +final class RepositoryResetTests: SwiftGitXTest { + @Test("Reset staged file") + func resetStagedFile() async throws { + let repository = mockRepository() + let initialCommit = try repository.mockCommit() + + // Create and stage a file + let file = try repository.mockFile() + try repository.add(file: file) + + #expect(try repository.status(file: file) == [.indexNew]) + + // Reset the staged file + try repository.reset(from: initialCommit, files: [file]) + + // File should be untracked now + let status = try repository.status(file: file) + #expect(status == [.workingTreeNew]) + } + + @Test("Soft reset to previous commit") + func resetSoft() async throws { + let repository = mockRepository() + let initialCommit = try repository.mockCommit() + + // Create another commit + try repository.mockCommit() + + // Reset to initial commit + try repository.reset(to: initialCommit) + + // HEAD should point to initial commit + let headCommit = try #require(repository.HEAD.target as? Commit) + #expect(headCommit == initialCommit) + } +} diff --git a/Tests/SwiftGitXTests/RepositoryTests/RepositoryRestoreTests.swift b/Tests/SwiftGitXTests/RepositoryTests/RepositoryRestoreTests.swift new file mode 100644 index 0000000..49e47ac --- /dev/null +++ b/Tests/SwiftGitXTests/RepositoryTests/RepositoryRestoreTests.swift @@ -0,0 +1,104 @@ +import Foundation +import SwiftGitX +import Testing + +@Suite("Repository - Restore", .tags(.repository, .operation, .restore)) +final class RepositoryRestoreTests: SwiftGitXTest { + @Test("Restore working tree file") + func restoreWorkingTree() async throws { + let repository = mockRepository() + + // Create and commit a file + let file1 = try repository.mockFile() + try repository.mockCommit(file: file1) + + // Modify the file (working tree change) + try Data("Restore me!".utf8).write(to: file1) + + // Create and stage another file (should not be restored) + let file2 = try repository.mockFile() + try repository.add(file: file2) + + // Restore + try repository.restore(files: [file1, file2]) + + // Verify file content is restored + let restoredContent = try String(contentsOf: file1) + #expect(restoredContent == "File 1 content\n") + + // Verify file2 is still staged + #expect(FileManager.default.fileExists(atPath: file2.path)) + #expect(try repository.status(file: file2) == [.indexNew]) + } + + @Test("Restore staged file") + func restoreStaged() async throws { + let repository = mockRepository() + + // Create and commit a file + let workingTreeFile = try repository.mockFile(name: "WorkingTree.md", content: "Hello, World!") + try repository.mockCommit(file: workingTreeFile) + + // Modify the file (should not be restored) + try Data("Should not be restored!".utf8).write(to: workingTreeFile) + + // Create and stage another file + let stagedFile = try repository.mockFile(name: "Stage.md", content: "Stage me!") + try repository.add(file: stagedFile) + + // Restore staged only + try repository.restore(.staged, paths: ["WorkingTree.md", "Stage.md"]) + + // Staged file should be unstaged + let stagedFileStatus = try repository.status(file: stagedFile) + #expect(stagedFileStatus == [.workingTreeNew]) + #expect(try String(contentsOf: stagedFile) == "Stage me!") + + // Working tree file should still be modified + let workingTreeFileStatus = try repository.status(file: workingTreeFile) + #expect(workingTreeFileStatus == [.workingTreeModified]) + #expect(try String(contentsOf: workingTreeFile) == "Should not be restored!") + } + + @Test("Restore both working tree and staged") + func restoreWorkingTreeAndStaged() async throws { + let repository = mockRepository() + + let file = try repository.mockFile() + try repository.mockCommit(file: file) + + // Modify file from mockCommit and stage it + try Data("Restore stage area!".utf8).write(to: file) + try repository.add(file: file) + + // Modify again (working tree change) + try Data("Restore working tree!".utf8).write(to: file) + + // Restore both + try repository.restore([.workingTree, .staged], files: [file]) + + // File should have no changes + let status = try repository.status(file: file) + #expect(status.isEmpty) + #expect(try String(contentsOf: file) == "File 1 content\n") + } + + @Test("Restore deletes untracked staged file") + func restoreDeletesUntrackedStagedFile() async throws { + let repository = mockRepository() + try repository.mockCommit() + + // Create and stage a new file + let fileToDelete = try repository.mockFile(name: "DeleteMe.md", content: "Delete me from stage area!") + try repository.add(file: fileToDelete) + + // Modify it + try Data("Delete me from working tree!".utf8).write(to: fileToDelete) + + // Restore both working tree and staged + try repository.restore([.workingTree, .staged], files: [fileToDelete]) + + // File should be deleted + #expect(FileManager.default.fileExists(atPath: fileToDelete.path) == false) + } +} diff --git a/Tests/SwiftGitXTests/RepositoryTests/RepositoryRevertTests.swift b/Tests/SwiftGitXTests/RepositoryTests/RepositoryRevertTests.swift new file mode 100644 index 0000000..efb96f8 --- /dev/null +++ b/Tests/SwiftGitXTests/RepositoryTests/RepositoryRevertTests.swift @@ -0,0 +1,136 @@ +import Foundation +import SwiftGitX +import Testing + +@Suite("Repository - Revert", .tags(.repository, .operation, .revert)) +final class RepositoryRevertTests: SwiftGitXTest { + @Test("Revert commit restores file content and stages changes") + func revertRestoresContent() async throws { + let repository = mockRepository() + + // Create initial commit with a file + let file1 = try repository.mockFile() + try repository.mockCommit(file: file1) + + // Modify file and commit the change we plan to revert + try Data("Modified content".utf8).write(to: file1) + try repository.add(file: file1) + let commitToRevert = try repository.commit(message: "Modify file") + + // Revert the commit + try repository.revert(commitToRevert) + + // File content should match the original commit + #expect(try String(contentsOf: file1) == "File 1 content\n") + + // Changes should be staged but not committed + #expect(try repository.status(file: file1) == [.indexModified]) + + // HEAD should point to the commit we reverted to. Because, + // git revert modifies the working tree/index but does not automatically create a new commit. + #expect(commitToRevert.id == (try repository.HEAD.target.id)) + } + + @Test("Revert commit that added a file stages deletion") + func revertAddedFile() async throws { + let repository = mockRepository() + try repository.mockCommit() + + // Create and commit a new file + let file2 = try repository.mockFile() + try repository.add(file: file2) + let commitToRevert = try repository.commit(message: "Add file2") + + try repository.revert(commitToRevert) + + // File should be removed from working tree and staged as deletion + #expect(FileManager.default.fileExists(atPath: file2.path) == false) + #expect(try repository.status(file: file2) == [.indexDeleted]) + } + + @Test("Revert and then commit creates a new revert commit") + func revertThenCommit() async throws { + let repository = mockRepository() + + // Setup: initial commit -> change commit + let file = try repository.mockFile() + let initialCommit = try repository.mockCommit(file: file) + + try Data("Changed".utf8).write(to: file) + try repository.add(file: file) + let changeCommit = try repository.commit(message: "Change file") + + // Revert change and create revert commit + try repository.revert(changeCommit) + let revertCommit = try repository.commit(message: "Revert \"\(changeCommit.summary)\"") + + // Verify revert commit message + #expect(revertCommit.message == "Revert \"Change file\"") + + // Revert commit should have change commit as parent + let parents = try revertCommit.parents + #expect(parents.count == 1) + #expect(parents.first == changeCommit) + + // File content should match initial commit + #expect(try String(contentsOf: file) == "File 1 content\n") + + // Verify commit order: revert -> change -> initial + let log = Array(try repository.log()) + #expect(log.count == 3) + #expect(log[0] == revertCommit) + #expect(log[1] == changeCommit) + #expect(log[2] == initialCommit) + } + + @Test("Revert commit with multiple file changes restores all files") + func revertMultipleFiles() async throws { + let repository = mockRepository() + + // Create initial commit with two files + let file1 = try repository.mockFile() + let file2 = try repository.mockFile() + try repository.add(files: [file1, file2]) + try repository.commit(message: "Initial commit") + + // Modify both files and commit + try Data("File 1 modified".utf8).write(to: file1) + try Data("File 2 modified".utf8).write(to: file2) + try repository.add(files: [file1, file2]) + let commitToRevert = try repository.commit(message: "Modify both files") + + try repository.revert(commitToRevert) + + // Both files should be restored and staged + #expect(try String(contentsOf: file1) == "File 1 content\n") + #expect(try String(contentsOf: file2) == "File 2 content\n") + #expect(try repository.status(file: file1) == [.indexModified]) + #expect(try repository.status(file: file2) == [.indexModified]) + } +} + +@Suite("Repository - Revert Errors", .tags(.repository, .operation, .revert, .error)) +final class RepositoryRevertErrorTests: SwiftGitXTest { + @Test("Revert fails when working tree has conflicting changes") + func revertConflictingWorkingTreeThrows() async throws { + let repository = mockRepository() + + // Create initial commit with a file + let file = try repository.mockFile() + try repository.mockCommit(file: file) + + // Commit change we plan to revert + try Data("Committed change".utf8).write(to: file) + try repository.add(file: file) + let commitToRevert = try repository.commit(message: "Change file") + + // Introduce conflicting working tree change + try Data("Conflicting working tree change".utf8).write(to: file) + + let error = #expect(throws: SwiftGitXError.self) { + try repository.revert(commitToRevert) + } + + #expect(error?.code.isConflict == true) + } +} diff --git a/Tests/SwiftGitXTests/RepositoryTests/RepositoryShowTests.swift b/Tests/SwiftGitXTests/RepositoryTests/RepositoryShowTests.swift index f0d4495..368eebc 100644 --- a/Tests/SwiftGitXTests/RepositoryTests/RepositoryShowTests.swift +++ b/Tests/SwiftGitXTests/RepositoryTests/RepositoryShowTests.swift @@ -1,11 +1,12 @@ -import XCTest +import SwiftGitX +import Testing -@testable import SwiftGitX - -final class RepositoryShowTests: SwiftGitXTestCase { - func testShowCommit() throws { +@Suite("Repository - Show", .tags(.repository, .operation, .show)) +final class RepositoryShowTests: SwiftGitXTest { + @Test("Show Commit") + func showCommit() throws { // Create mock repository at the temporary directory - let repository = Repository.mock(named: "test-show-commit", in: Self.directory) + let repository = mockRepository() // Create a new commit let commit = try repository.mockCommit() @@ -14,12 +15,13 @@ final class RepositoryShowTests: SwiftGitXTestCase { let commitShowed: Commit = try repository.show(id: commit.id) // Check if the commit is the same - XCTAssertEqual(commit, commitShowed) + #expect(commit == commitShowed) } - func testShowTag() throws { + @Test("Show Tag") + func showTag() throws { // Create mock repository at the temporary directory - let repository = Repository.mock(named: "test-show-tag", in: Self.directory) + let repository = mockRepository() // Create a new commit let commit = try repository.mockCommit() @@ -28,15 +30,16 @@ final class RepositoryShowTests: SwiftGitXTestCase { let tag = try repository.tag.create(named: "v1.0.0", target: commit) // Get the tag by id - let tagShowed: Tag = try repository.show(id: tag.id) + let tagShowed: SwiftGitX.Tag = try repository.show(id: tag.id) // Check if the tag is the same - XCTAssertEqual(tag, tagShowed) + #expect(tag == tagShowed) } - func testShowTree() throws { + @Test("Show Tree") + func showTree() throws { // Create mock repository at the temporary directory - let repository = Repository.mock(named: "test-show-tree", in: Self.directory) + let repository = mockRepository() // Create a new commit let commit = try repository.mockCommit() @@ -48,35 +51,39 @@ final class RepositoryShowTests: SwiftGitXTestCase { let treeShowed: Tree = try repository.show(id: tree.id) // Check if the tree is the same - XCTAssertEqual(tree, treeShowed) + #expect(tree == treeShowed) } - func testShowBlob() throws { + @Test("Show Blob") + func showBlob() throws { // Create mock repository at the temporary directory - let repository = Repository.mock(named: "test-show-blob", in: Self.directory) + let repository = mockRepository() // Create a new commit let commit = try repository.mockCommit() // Get the blob of the file - let blob = try XCTUnwrap(commit.tree.entries.first) + let blob = try #require(commit.tree.entries.first) // Get the blob by id let blobShowed: Blob = try repository.show(id: blob.id) // Check if the blob properties are the same - XCTAssertEqual(blob.id, blobShowed.id) - XCTAssertEqual(blob.type, blobShowed.type) + #expect(blob.id == blobShowed.id) + #expect(blob.type == blobShowed.type) } - func testShowInvalidObjectType() throws { + @Test("Show Invalid Object Type Should Fail") + func showInvalidObjectType() throws { // Create mock repository at the temporary directory - let repository = Repository.mock(named: "test-show-invalid-object-type", in: Self.directory) + let repository = mockRepository() // Create a new commit let commit = try repository.mockCommit() // Try to show a commit as a tree - XCTAssertThrowsError(try repository.show(id: commit.id) as Tree) + #expect(throws: Error.self) { + try repository.show(id: commit.id) as Tree + } } } diff --git a/Tests/SwiftGitXTests/RepositoryTests/RepositoryStatusTests.swift b/Tests/SwiftGitXTests/RepositoryTests/RepositoryStatusTests.swift new file mode 100644 index 0000000..6eedde3 --- /dev/null +++ b/Tests/SwiftGitXTests/RepositoryTests/RepositoryStatusTests.swift @@ -0,0 +1,101 @@ +import Foundation +import SwiftGitX +import Testing + +@Suite("Repository - Status", .tags(.repository, .operation, .status)) +final class RepositoryStatusTests: SwiftGitXTest { + @Test("Repository status untracked") + func repositoryStatusUntracked() async throws { + let repository = mockRepository() + + // Create a new file in the repository + _ = try repository.mockFile() + + // Get the status of the repository + let status = try repository.status() + + // Check the status of the repository + #expect(status.count == 1) + + // Get the status entry + let statusEntry = try #require(status.first) + + // Check the status entry properties + #expect(statusEntry.status == [.workingTreeNew]) + #expect(statusEntry.index == nil) // There is no index changes + + // Get working tree changes + let workingTreeChanges = try #require(statusEntry.workingTree) + + // Check the status entry diff delta properties + #expect(workingTreeChanges.type == .untracked) + + #expect(workingTreeChanges.newFile.path == "file-1.txt") + #expect(workingTreeChanges.oldFile.path == "file-1.txt") + + #expect(workingTreeChanges.newFile.size == "File 1 content\n".count) + #expect(workingTreeChanges.oldFile.size == 0) + } + + @Test("Repository status added") + func repositoryStatusAdded() async throws { + let repository = mockRepository() + + // Create a new file in the repository + let file = try repository.mockFile() + + // Add the file + try repository.add(file: file) + + // Get the status of the repository + let status = try repository.status() + + // Check the status of the repository + #expect(status.count == 1) + + // Get the status entry + let statusEntry = try #require(status.first) + + // Check the status entry properties + #expect(statusEntry.status == [.indexNew]) + #expect(statusEntry.workingTree == nil) // There is no working tree changes + let statusEntryDiffDelta = try #require(statusEntry.index) + + // Check the status entry diff delta properties + #expect(statusEntryDiffDelta.type == .added) + + #expect(statusEntryDiffDelta.newFile.path == "file-1.txt") + #expect(statusEntryDiffDelta.oldFile.path == "file-1.txt") + + #expect(statusEntryDiffDelta.newFile.size == "File 1 content\n".count) + #expect(statusEntryDiffDelta.oldFile.size == 0) + + // Get the blob of the new file + let blob: Blob = try repository.show(id: statusEntryDiffDelta.newFile.id) + let blobText = try #require(String(data: blob.content, encoding: .utf8)) + #expect(blobText == "File 1 content\n") + } + + @Test("Repository status file new and modified") + func repositoryStatusFileNewAndModified() async throws { + let repository = mockRepository() + + // Create a new file in the repository + let file = try repository.mockFile() + + // Add the file + try repository.add(file: file) + + // Modify the file + try Data("Merhaba, Dünya!".utf8).write(to: file) + + // Get the status of the repository + let status: [StatusEntry.Status] = try repository.status(file: file) + + // Check the status of the repository + #expect(status.count == 2) + + // Check the status entry properties + #expect(status == [.indexNew, .workingTreeModified]) + } +} diff --git a/Tests/SwiftGitXTests/RepositoryTests/RepositorySwitchTests.swift b/Tests/SwiftGitXTests/RepositoryTests/RepositorySwitchTests.swift index 0e85413..768c4b2 100644 --- a/Tests/SwiftGitXTests/RepositoryTests/RepositorySwitchTests.swift +++ b/Tests/SwiftGitXTests/RepositoryTests/RepositorySwitchTests.swift @@ -1,31 +1,33 @@ +import Foundation import SwiftGitX -import XCTest +import Testing -final class RepositorySwitchTests: SwiftGitXTestCase { - func testRepositorySwitchBranch() throws { +@Suite("Repository - Switch", .tags(.repository, .operation, .switch)) +final class RepositorySwitchTests: SwiftGitXTest { + @Test("Switch to Branch") + func switchBranch() throws { // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-switch-branch", in: Self.directory) - - // Create mock commit + let repository = mockRepository() let commit = try repository.mockCommit() // Create a new branch let branch = try repository.branch.create(named: "feature", target: commit) // Switch the new branch - XCTAssertNoThrow(try repository.switch(to: branch)) + try repository.switch(to: branch) // Get the HEAD reference let head = try repository.HEAD // Check the HEAD reference - XCTAssertEqual(head.name, branch.name) - XCTAssertEqual(head.fullName, branch.fullName) + #expect(head.name == branch.name) + #expect(head.fullName == branch.fullName) } - func testRepositorySwitchBranchGuess() async throws { + @Test("Switch to Remote Branch (Fresh Clone)") + func switchBranchGuess() async throws { let source = URL(string: "https://github.com/ibrahimcetin/PassbankMD.git")! - let repositoryDirectory = Repository.mockDirectory(named: "test-switch-branch-guess", in: Self.directory) + let repositoryDirectory = mockDirectory() let repository = try await Repository.clone(from: source, to: repositoryDirectory) // Switch to the branch @@ -36,89 +38,87 @@ final class RepositorySwitchTests: SwiftGitXTestCase { let head = try repository.HEAD // Check the HEAD reference - XCTAssertEqual(head.name, remoteBranch.name.replacingOccurrences(of: "origin/", with: "")) - XCTAssertEqual(head.target as? Commit, remoteBranch.target as? Commit) + #expect(head.name == remoteBranch.name.replacing("origin/", with: "")) + #expect(head.target as? Commit == remoteBranch.target as? Commit) } - func testRepositorySwitchCommit() throws { + @Test("Switch to Commit") + func switchCommit() throws { // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-switch-commit", in: Self.directory) - - // Create mock commit + let repository = mockRepository() let commit = try repository.mockCommit() // Switch to the commit - XCTAssertNoThrow(try repository.switch(to: commit)) + try repository.switch(to: commit) // Get the HEAD reference let head = try repository.HEAD // Check the HEAD reference (detached HEAD) - XCTAssertTrue(repository.isHEADDetached) + #expect(repository.isHEADDetached) - XCTAssertEqual(head.name, "HEAD") - XCTAssertEqual(head.fullName, "HEAD") + #expect(head.name == "HEAD") + #expect(head.fullName == "HEAD") } - func testRepositorySwitchTagAnnotated() throws { + @Test("Switch to Annotated Tag") + func switchTagAnnotated() throws { // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-switch-tag-annotated", in: Self.directory) - - // Create mock commit + let repository = mockRepository() let commit = try repository.mockCommit() // Create a new tag let tag = try repository.tag.create(named: "v1.0.0", target: commit) // Switch to the tag - XCTAssertNoThrow(try repository.switch(to: tag)) + try repository.switch(to: tag) // Get the HEAD reference let head = try repository.HEAD // Check the HEAD reference - XCTAssertEqual(head.name, tag.name) - XCTAssertEqual(head.fullName, tag.fullName) + #expect(head.name == tag.name) + #expect(head.fullName == tag.fullName) } - func testRepositorySwitchTagLightweight() throws { + @Test("Switch to Lightweight Tag") + func switchTagLightweight() throws { // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-switch-tag-lightweight", in: Self.directory) - - // Create mock commit + let repository = mockRepository() let commit = try repository.mockCommit() // Create a new tag let tag = try repository.tag.create(named: "v1.0.0", target: commit, type: .lightweight) // When a lightweight tag is created, the tag ID is the same as the commit ID - XCTAssertEqual(tag.id, commit.id) + #expect(tag.id == commit.id) // Switch to the tag - XCTAssertNoThrow(try repository.switch(to: tag)) + try repository.switch(to: tag) // Get the HEAD reference let head = try repository.HEAD // Check the HEAD reference - XCTAssertEqual(head.target.id, tag.id) + #expect(head.target.id == tag.id) - XCTAssertEqual(head.name, tag.name) - XCTAssertEqual(head.fullName, tag.fullName) + #expect(head.name == tag.name) + #expect(head.fullName == tag.fullName) } - func testRepositorySwitchTagLightweightTreeFailure() throws { + @Test("Switch to Lightweight Tag Tree Should Fail") + func switchTagLightweightTreeFailure() throws { // Create a new repository at the temporary directory - let repository = Repository.mock(named: "test-switch-tag-lightweight-tree-failure", in: Self.directory) - - // Create mock commit + let repository = mockRepository() let commit = try repository.mockCommit() // Create a new tag let tag = try repository.tag.create(named: "v1.0.0", target: commit.tree, type: .lightweight) // Switch to the tag - XCTAssertThrowsError(try repository.switch(to: tag)) + #expect(throws: Error.self) { + try repository.switch(to: tag) + } } // TODO: Add test for remote branch checkout diff --git a/Tests/SwiftGitXTests/RepositoryTests/RepositoryTests.swift b/Tests/SwiftGitXTests/RepositoryTests/RepositoryTests.swift index 4c391b4..05720c9 100644 --- a/Tests/SwiftGitXTests/RepositoryTests/RepositoryTests.swift +++ b/Tests/SwiftGitXTests/RepositoryTests/RepositoryTests.swift @@ -1,76 +1,35 @@ +import Foundation import SwiftGitX -import XCTest +import Testing extension Repository { - static var testsDirectory: URL { - URL.temporaryDirectory.appending(components: "SwiftGitXTests") - } - - /// Creates a new mock repository at the temporary directory with the given name. + /// Creates a mock file in the repository. /// - /// - Parameters - /// - name: The name of the mock repository to create. - /// - parentDirectoryName: The name of the parent directory to create the repository in. + /// - Parameters: + /// - name: The name of the file. If nil, generates a unique sequential name. + /// - content: The content of the file. If nil, generates sequential content. /// - /// - Returns: The created repository. - static func mock(named name: String, in parentDirectoryName: String, isBare: Bool = false) -> Repository { - do { - let directory = mockDirectory(named: name, in: parentDirectoryName) - - // Create a new repository at the temporary directory - return try Repository.create(at: directory, isBare: isBare) - } catch { - fatalError("Failed to create a mock repository: \(error)") - } - } + /// - Returns: The URL of the created file. + func mockFile(name: String? = nil, content: String? = nil) throws -> URL { + // Count existing files in working directory to determine sequence number + let existingFiles = try FileManager.default.contentsOfDirectory( + at: workingDirectory, + includingPropertiesForKeys: nil + ).filter { !$0.lastPathComponent.hasPrefix(".") } // Exclude hidden files - /// Creates an empty directory with the given name in the temporary directory. - /// - /// - Parameters - /// - name: The name of the directory to create. - /// - parentDirectoryName: The name of the parent directory to create the directory in. - /// - create: Whether to create the directory or not. - /// - /// - Returns: The URL of the directory. - /// - /// If the directory already exists, it always will be removed. - /// If the `create` parameter is set to `true`, the directory will be created. - /// Otherwise, only the URL of the empty directory will be returned. - static func mockDirectory(named name: String, in parentDirectoryName: String, create: Bool = false) -> URL { - do { - // Create a new directory url in the temporary directory - let directory = - if parentDirectoryName.isEmpty { - Self.testsDirectory.appending(components: name) - } else { - Self.testsDirectory.appending(components: parentDirectoryName, name) - } - - // Remove the directory if it already exists to create an empty repository - if FileManager.default.fileExists(atPath: directory.path) { - try FileManager.default.removeItem(at: directory) - } - - // Create the directory - if create { - try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false) - } - - return directory - } catch { - fatalError("Failed to create a mock directory: \(error)") - } - } + let sequenceNumber = existingFiles.count + 1 - /// Creates a mock file in the repository. - /// - /// - Parameter name: The name of the file - func mockFile(named name: String, content: String? = nil) throws -> URL { - let file = try workingDirectory.appending(component: name) + // Generate a unique file name if none is provided + let fileName = name ?? "file-\(sequenceNumber).txt" + + // Generate sequential content if none is provided + let fileContent = content ?? "File \(sequenceNumber) content\n" + + let file = try workingDirectory.appending(component: fileName) FileManager.default.createFile( atPath: file.path, - contents: (content ?? "Welcome to SwiftGitX!\n").data(using: .utf8) + contents: fileContent.data(using: .utf8) ) return file @@ -79,18 +38,32 @@ extension Repository { /// Creates a mock commit in the repository. @discardableResult func mockCommit(message: String? = nil, file: URL? = nil) throws -> Commit { + // Count existing commits to determine the sequence number + let commitCount = (try? log().reduce(0) { count, _ in count + 1 }) ?? 0 + let sequenceNumber = commitCount + 1 + + // Generate a unique file if none is provided to ensure we always have changes to commit + let fileToAdd = try file ?? mockFile(name: "file-\(sequenceNumber).txt") + // Add the file to the index - try add(file: file ?? mockFile(named: "README.md")) + try add(file: fileToAdd) + + // Determine the commit message based on sequence + let commitMessage = message ?? "Commit #\(sequenceNumber)" // Commit the changes - return try commit(message: message ?? "Initial commit") + return try commit(message: commitMessage) } } -final class RepositoryTests: SwiftGitXTestCase { - func testRepositoryInit() throws { +// MARK: - Repository Initialization + +@Suite("Repository - Initialization", .tags(.repository)) +final class RepositoryInitializationTests: SwiftGitXTest { + @Test("Repository init creates or opens repository") + func repositoryInit() async throws { // Create a temporary directory for the repository - let directory = Repository.mockDirectory(named: "test-init", in: Self.directory) + let directory = mockDirectory() // This should create a new repository at the empty directory let repositoryCreated = try Repository(at: directory) @@ -108,169 +81,68 @@ final class RepositoryTests: SwiftGitXTestCase { // Check if the HEAD commit is the same as the created commit // This checks if the repository was created and opened successfully // This also ensures that the second call to `Repository(at:)` opens the existing repository - XCTAssertEqual(commit, headCommit) + #expect(commit == headCommit) } - func testRepositoryCreate() { + @Test("Repository create") + func repositoryCreate() async throws { // Create a temporary directory for the repository - let directory = Repository.mockDirectory(named: "test-create", in: Self.directory) + let directory = mockDirectory() // Create a new repository at the temporary directory - XCTAssertNoThrow(try Repository.create(at: directory)) + _ = try Repository.create(at: directory) // Check if the repository opens without any errors - XCTAssertNoThrow(try Repository(at: directory)) + _ = try Repository(at: directory) } - func testRepositoryCreateBare() throws { + @Test("Repository create bare") + func repositoryCreateBare() async throws { // Create a temporary directory for the repository - let directory = Repository.mockDirectory(named: "test-create-bare", in: Self.directory) + let directory = mockDirectory() // Create a new repository at the temporary directory - XCTAssertNoThrow(try Repository.create(at: directory, isBare: true)) + _ = try Repository.create(at: directory, isBare: true) // Check if the repository opens without any errors let repository = try Repository(at: directory) // Check if the repository is bare - XCTAssertTrue(repository.isBare) + #expect(repository.isBare) } - func testRepositoryOpen() { + @Test("Repository open") + func repositoryOpen() async throws { // Create a temporary directory for the repository - let directory = Repository.mockDirectory(named: "test-open", in: Self.directory) + let directory = mockDirectory() // Create a new repository at the temporary directory - XCTAssertNoThrow(try Repository.create(at: directory)) + _ = try Repository.create(at: directory) // Check if the repository opens without any errors - XCTAssertNoThrow(try Repository.open(at: directory)) + _ = try Repository.open(at: directory) } - func testRepositoryOpenFailure() { + @Test("Repository open failure") + func repositoryOpenFailure() async throws { // Create a temporary directory for the repository - let directory = Repository.mockDirectory(named: "test-non-existent", in: Self.directory, create: true) - - // Try to create a repository at a non-existent directory - try XCTAssertThrowsError(Repository.open(at: directory)) - } - - func testRepositoryClone() async throws { - // Create a temporary URL for the source repository - let source = URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! - - // Create a temporary directory for the destination repository - let directory = Repository.mockDirectory(named: "test-clone", in: Self.directory) - - // Perform the clone operation - _ = try await Repository.clone(from: source, to: directory) - - // Check if the destination repository exists - XCTAssertTrue(FileManager.default.fileExists(atPath: directory.path)) - - // Check if the repository opens without any errors - XCTAssertNoThrow(try Repository(at: directory)) - } - - func testRepositoryCloneCancellation() async { - // Create a temporary URL for the source repository - let source = URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! - - // Create a temporary directory for the destination repository - let directory = Repository.mockDirectory(named: "test-clone-cancellation", in: Self.directory) - - // Perform the clone operation - let task = Task { - try await Repository.clone(from: source, to: directory) - } - - // Cancel the task - task.cancel() - - // Wait for the task to complete - let result = await task.result - - // Check if the task is cancelled - XCTAssertTrue(task.isCancelled) + let directory = mockDirectory() - // Check if the task result is a failure - guard case .failure = result else { - XCTFail("The task should be cancelled.") - return + // Try to open a repository at a non-repository directory + #expect(throws: SwiftGitXError.self) { + try Repository.open(at: directory) } - - // Check if the destination repository exists - XCTAssertFalse(FileManager.default.fileExists(atPath: directory.path)) - } - - func testRepositoryCloneWithProgress() async throws { - // Create source URL for the repository - let source = URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! - - // Create a temporary directory for the destination repository - let directory = Repository.mockDirectory(named: "test-clone-progress", in: Self.directory) - - let progressExpectation = expectation(description: "Cloning progress") - - // Perform the clone operation - _ = try await Repository.clone(from: source, to: directory) { progress in - guard progress.indexedDeltas == progress.totalDeltas else { return } - - guard progress.receivedObjects == progress.totalObjects else { return } - - guard progress.indexedObjects == progress.totalObjects else { return } - - progressExpectation.fulfill() - } - - // Wait for the progress to complete - await fulfillment(of: [progressExpectation], timeout: 60) - - // Check if the destination repository exists - XCTAssertTrue(FileManager.default.fileExists(atPath: directory.path)) - - // Check if the repository opens without any errors - XCTAssertNoThrow(try Repository(at: directory)) } +} - func testRepositoryCloneWithProgressCancellation() async throws { - // Create source URL for the repository - let source = URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! - - // Create a temporary directory for the destination repository - let directory = Repository.mockDirectory(named: "test-clone-progress-cancellation", in: Self.directory) - - // Create a task for the clone operation - let task = Task { - let repository = try await Repository.clone(from: source, to: directory) { progress in - print(progress) - } - - return repository - } - - // Cancel the task - task.cancel() - - // Wait for the task to complete (shouldn't wait because cancelled) - let result = await task.result - - // Check if the task is cancelled - XCTAssertTrue(task.isCancelled) - - // Check if the task result is a failure - guard case .failure = result else { - XCTFail("The task should be cancelled.") - return - } - - // Check if the destination repository exists - XCTAssertFalse(FileManager.default.fileExists(atPath: directory.path)) - } +// MARK: - Repository Protocols - func testRepositoryCodable() throws { +@Suite("Repository - Protocols", .tags(.repository)) +final class RepositoryProtocolTests: SwiftGitXTest { + @Test("Repository Codable") + func repositoryCodable() async throws { // Create a repository at the temporary directory - let repository = Repository.mock(named: "test-codable", in: Self.directory) + let repository = mockRepository() // Create a new commit try repository.mockCommit() @@ -282,12 +154,13 @@ final class RepositoryTests: SwiftGitXTestCase { let decodedRepository = try JSONDecoder().decode(Repository.self, from: data) // Check if the decoded repository HEAD is the same as the original repository HEAD - try XCTAssertEqual(repository.HEAD as! Branch, decodedRepository.HEAD as! Branch) + #expect(try (repository.HEAD as! Branch) == (decodedRepository.HEAD as! Branch)) } - func testRepositoryEquatable() throws { + @Test("Repository Equatable") + func repositoryEquatable() async throws { // Create a repository at the temporary directory - let repository = Repository.mock(named: "test-equatable", in: Self.directory) + let repository = mockRepository() // Create a new commit try repository.mockCommit() @@ -296,15 +169,16 @@ final class RepositoryTests: SwiftGitXTestCase { let anotherRepository = try Repository(at: repository.path) // Check if the repository HEADs are the same - try XCTAssertEqual(repository.HEAD as! Branch, anotherRepository.HEAD as! Branch) + #expect(try (repository.HEAD as! Branch) == (anotherRepository.HEAD as! Branch)) // Check if the repositories are equal - XCTAssertEqual(repository, anotherRepository) + #expect(repository == anotherRepository) } - func testRepositoryHashable() throws { + @Test("Repository Hashable") + func repositoryHashable() async throws { // Create a repository at the temporary directory - let repository = Repository.mock(named: "test-hashable", in: Self.directory) + let repository = mockRepository() // Create a new commit try repository.mockCommit() @@ -313,9 +187,9 @@ final class RepositoryTests: SwiftGitXTestCase { let anotherRepository = try Repository(at: repository.path) // Check if the repository HEADs are the same - try XCTAssertEqual(repository.HEAD as! Branch, anotherRepository.HEAD as! Branch) + #expect(try (repository.HEAD as! Branch) == (anotherRepository.HEAD as! Branch)) // Check if the repositories have the same hash value - XCTAssertEqual(repository.hashValue, anotherRepository.hashValue) + #expect(repository.hashValue == anotherRepository.hashValue) } } diff --git a/Tests/SwiftGitXTests/SwiftGitXTests.swift b/Tests/SwiftGitXTests/SwiftGitXTests.swift index 83d8256..fc46be5 100644 --- a/Tests/SwiftGitXTests/SwiftGitXTests.swift +++ b/Tests/SwiftGitXTests/SwiftGitXTests.swift @@ -1,36 +1,80 @@ +import Foundation import SwiftGitX import Testing -import XCTest - -class SwiftGitXTestCase: XCTestCase { - static var directory: String { - String(describing: Self.self) - } - - override class func setUp() { - super.setUp() - - // Initialize the SwiftGitX library - XCTAssertNoThrow(try SwiftGitXRuntime.initialize()) - } - - override class func tearDown() { - // Shutdown the SwiftGitX library - XCTAssertNoThrow(try SwiftGitXRuntime.shutdown()) - - // Remove the temporary directory for the tests - try? FileManager.default.removeItem(at: Repository.testsDirectory.appending(component: directory)) - - super.tearDown() - } -} /// Base class for SwiftGitX tests to initialize and shutdown the library /// /// - Important: Inherit from this class to create a test suite. class SwiftGitXTest { - static var directory: String { - String(describing: Self.self) + /// Creates a new mock repository with auto-generated unique name based on the calling test. + /// + /// This method automatically generates a unique repository name using the file and function + /// where it's called, making it perfect for parallel test execution. + /// + /// - Parameters: + /// - fileID: Automatically captured file identifier. + /// - name: The name of the mock repository to create. + /// - suffix: Suffix to add to the directory name. + /// - isBare: Whether to create a bare repository. + /// + /// - Returns: The created repository. + func mockRepository( + fileID: String = #fileID, + name: String = #function, + suffix: String = "", + isBare: Bool = false + ) -> Repository { + // Create a new mock directory + let directory = mockDirectory(fileID: fileID, name: name, suffix: suffix) + + // Create the repository + return try! Repository.create(at: directory, isBare: isBare) + } + + /// Creates a new mock directory with auto-generated unique name based on the calling test. + /// + /// This method automatically generates a unique directory name using the file and function + /// where it's called, making it perfect for parallel test execution. + /// + /// - Parameters: + /// - fileID: Automatically captured file identifier. + /// - name: The name of the mock directory to create. + /// - suffix: Suffix to add to the directory name. + /// - create: Whether to create the directory or not (default: false). + /// + /// - Returns: The created directory. + func mockDirectory( + fileID: String = #fileID, + name: String = #function, + suffix: String = "", + create: Bool = false + ) -> URL { + // Get the suite name + let suiteName = String(describing: Self.self) + + // Extract file name from fileID + // fileID format: "SwiftGitXTests/Collections/BranchCollectionTests.swift" + let fileName = fileID.components(separatedBy: "/").last!.replacing(".swift", with: "") + + // Extract name + // name format: "testBranchLookup()" or "branchLookup()" + let directoryName = name.replacing("()", with: "").replacing("test", with: "") + suffix + + // Create the directory + let directory = URL.temporaryDirectory + .appending(components: "SwiftGitXTests", fileName, suiteName, directoryName) + + // Remove the directory if it already exists to create an empty repository + if FileManager.default.fileExists(atPath: directory.path) { + try! FileManager.default.removeItem(at: directory) + } + + // Create the directory + if create { + try! FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false) + } + + return directory } init() throws { @@ -42,38 +86,39 @@ class SwiftGitXTest { } } -// Test the SwiftGitX struct to initialize and shutdown the library -@Suite("SwiftGitX Tests", .tags(.swiftGitX), .serialized) -struct SwiftGitXTests { - @Test("Test SwiftGitX Initialize") - func testSwiftGitXInitialize() async throws { - // Initialize the SwiftGitX library +// Test the SwiftGitXRuntime enum to initialize and shutdown the library +@Suite("SwiftGitX Runtime Tests", .tags(.runtime), .serialized) +struct SwiftGitXRuntimeTests { + @Test("Test SwiftGitXRuntime Initialize") + func initialize() async throws { + // Initialize the SwiftGitXRuntime let count = try SwiftGitXRuntime.initialize() // Check if the initialization count is valid #expect(count > 0) } - @Test("Test SwiftGitX Shutdown") - func testSwiftGitXShutdown() async throws { - // Shutdown the SwiftGitX library + @Test("Test SwiftGitXRuntime Shutdown") + func shutdown() async throws { + // Shutdown the SwiftGitXRuntime let count = try SwiftGitXRuntime.shutdown() // Check if the shutdown count is valid #expect(count >= 0) } - @Test("Test SwiftGitX Shutdown Without Calling Initialize") - func testSwiftGitXShutdownWithoutInitialize() async throws { - // Shutdown the SwiftGitX library - let result = #expect(throws: SwiftGitXError.self) { + @Test( + "Test SwiftGitXRuntime Shutdown Without Calling Initialize", + .disabled("This test is disabled because it should be skipped while running all tests. Enable if you want.") + ) + func shutdownWithoutInitialize() async throws { + // Shutdown the SwiftGitXRuntime + let error = #expect(throws: SwiftGitXError.self) { try SwiftGitXRuntime.shutdown() } - let error = try #require(result) - // Check if the error is a SwiftGitXError - #expect(error.code == .error) + #expect(error?.code == .error) // Note: This is a quirk of libgit2's design. When shutdown() is called before initialize(), // it decrements the initialization count below 0 and returns a negative status code (error), @@ -82,12 +127,12 @@ struct SwiftGitXTests { // // We still throw an error because shutdown should not be called without initialize, even though // the error message is uninformative. This error can be ignored if needed. - #expect(error.category == .none) - #expect(error.message == "no error") + #expect(error?.category == SwiftGitXError.Category.none) + #expect(error?.message == "no error") } - @Test("Test SwiftGitX Version") - func testVersion() throws { + @Test("Test libgit2 version") + func libgit2Version() throws { // Get the libgit2 version let version = SwiftGitXRuntime.libgit2Version @@ -95,7 +140,3 @@ struct SwiftGitXTests { #expect(version == "1.9.0") } } - -extension Testing.Tag { - @Tag static var swiftGitX: Self -} diff --git a/Tests/SwiftGitXTests/Tags.swift b/Tests/SwiftGitXTests/Tags.swift new file mode 100644 index 0000000..4bcd19d --- /dev/null +++ b/Tests/SwiftGitXTests/Tags.swift @@ -0,0 +1,40 @@ +import Testing + +extension Testing.Tag { + // Categories + @Tag static var collection: Self + @Tag static var error: Self + @Tag static var model: Self + @Tag static var operation: Self + @Tag static var repository: Self + @Tag static var runtime: Self + + // Operations + @Tag static var add: Self + @Tag static var clone: Self + @Tag static var commit: Self + @Tag static var diff: Self + @Tag static var fetch: Self + @Tag static var log: Self + @Tag static var patch: Self + @Tag static var push: Self + @Tag static var reset: Self + @Tag static var restore: Self + @Tag static var revert: Self + @Tag static var show: Self + @Tag static var status: Self + @Tag static var `switch`: Self + + // Collections + @Tag static var branch: Self + @Tag static var config: Self + @Tag static var index: Self + @Tag static var reference: Self + @Tag static var remote: Self + @Tag static var stash: Self + @Tag static var tag: Self + + // Models + @Tag static var oid: Self + @Tag static var signature: Self +}