-
Notifications
You must be signed in to change notification settings - Fork 0
test: add e2e test suite for CLI binary-level testing #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
3f9acf9
test: add e2e test suite for CLI binary-level testing
alexandreafj bb982ad
fix: resolve golangci-lint issues in e2e tests
alexandreafj c124367
fix: address PR review comments on e2e test suite
alexandreafj b56a1f2
fix: address second round of PR review comments
alexandreafj File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,215 @@ | ||
| package e2e | ||
|
|
||
| import ( | ||
| "path/filepath" | ||
| "testing" | ||
| ) | ||
|
|
||
| // ========================================================================== | ||
| // Phase 3: Branch Operations (gitm branch create/rename) | ||
| // ========================================================================== | ||
|
|
||
| func TestBranchCreate_WithRepo(t *testing.T) { | ||
| e := newTestEnv(t) | ||
| repo, _ := e.initRepoWithRemote("bc-repo") | ||
| e.runGitm("repo", "add", repo, "--alias", "bc-repo") | ||
|
|
||
| r := e.runGitm("branch", "create", "feat/test-branch", "--repo", "bc-repo") | ||
| e.assertExitCode(r, 0) | ||
|
|
||
| // Verify branch was created and we're on it | ||
| branch := e.currentBranch(repo) | ||
| if branch != "feat/test-branch" { | ||
| t.Errorf("expected to be on feat/test-branch, got %s", branch) | ||
| } | ||
| } | ||
|
|
||
| func TestBranchCreate_WithAll(t *testing.T) { | ||
| e := newTestEnv(t) | ||
| repo1, _ := e.initRepoWithRemote("bc-all-1") | ||
| repo2, _ := e.initRepoWithRemote("bc-all-2") | ||
| e.runGitm("repo", "add", repo1, "--alias", "bc-all-1") | ||
| e.runGitm("repo", "add", repo2, "--alias", "bc-all-2") | ||
|
|
||
| r := e.runGitm("branch", "create", "feat/all-branch", "--all") | ||
| e.assertExitCode(r, 0) | ||
|
|
||
| // Both repos should have the branch | ||
| if !e.branchExists(repo1, "feat/all-branch") { | ||
| t.Error("branch not created in repo1") | ||
| } | ||
| if !e.branchExists(repo2, "feat/all-branch") { | ||
| t.Error("branch not created in repo2") | ||
| } | ||
| } | ||
|
|
||
| func TestBranchCreate_FromSpecificBase(t *testing.T) { | ||
| e := newTestEnv(t) | ||
| repo, _ := e.initRepoWithRemote("bc-from") | ||
| e.runGitm("repo", "add", repo, "--alias", "bc-from") | ||
|
|
||
| // Create a develop branch first | ||
| e.mustGit(repo, "checkout", "-b", "develop") | ||
| e.writeFile(repo, "develop.txt", "develop content\n") | ||
| e.mustGit(repo, "add", ".") | ||
| e.mustGit(repo, "commit", "-m", "develop commit") | ||
| e.mustGit(repo, "push", "--set-upstream", "origin", "develop") | ||
| e.mustGit(repo, "checkout", "main") | ||
|
|
||
| r := e.runGitm("branch", "create", "feat/from-develop", "--from", "develop", "--repo", "bc-from") | ||
| e.assertExitCode(r, 0) | ||
|
|
||
| // Should have the develop.txt file (branched from develop) | ||
| if !e.fileExists(filepath.Join(repo, "develop.txt")) { | ||
| t.Error("branch was not created from develop — develop.txt missing") | ||
| } | ||
| } | ||
|
|
||
| func TestBranchCreate_ExistingBranch(t *testing.T) { | ||
| e := newTestEnv(t) | ||
| repo, _ := e.initRepoWithRemote("bc-existing") | ||
| e.runGitm("repo", "add", repo, "--alias", "bc-existing") | ||
|
|
||
| // Create the branch first | ||
| e.mustGit(repo, "checkout", "-b", "feat/already-exists") | ||
| e.mustGit(repo, "checkout", "main") | ||
|
|
||
| // gitm should check it out instead of erroring | ||
| r := e.runGitm("branch", "create", "feat/already-exists", "--repo", "bc-existing") | ||
| e.assertExitCode(r, 0) | ||
|
|
||
| branch := e.currentBranch(repo) | ||
| if branch != "feat/already-exists" { | ||
| t.Errorf("expected to be on feat/already-exists, got %s", branch) | ||
| } | ||
| } | ||
|
|
||
| func TestBranchCreate_FromNonExistentBase(t *testing.T) { | ||
| e := newTestEnv(t) | ||
| repo, _ := e.initRepoWithRemote("bc-bad-base") | ||
| e.runGitm("repo", "add", repo, "--alias", "bc-bad-base") | ||
|
|
||
| startingBranch := e.currentBranch(repo) | ||
| r := e.runGitm("branch", "create", "feat/x", "--from", "nonexistent-branch", "--repo", "bc-bad-base") | ||
|
|
||
| combined := r.Stdout + r.Stderr | ||
| // Accept either an explicit command failure or a successful exit that clearly reports | ||
| // the invalid base branch. Silent success is incorrect. | ||
| if r.ExitCode == 0 && !containsAny(combined, "not found", "error", "failed", "does not exist") { | ||
| t.Fatalf("expected branch create from non-existent base to fail or report the missing base branch; exit=%d stdout=%q stderr=%q", | ||
| r.ExitCode, r.Stdout, r.Stderr) | ||
| } | ||
|
|
||
| if e.branchExists(repo, "feat/x") { | ||
| t.Fatal("branch feat/x should not be created when the base branch does not exist") | ||
| } | ||
|
|
||
| if branch := e.currentBranch(repo); branch != startingBranch { | ||
| t.Fatalf("expected to remain on %s after failing to create from a non-existent base, got %s", startingBranch, branch) | ||
| } | ||
| } | ||
|
|
||
| func TestBranchCreate_DirtyRepo(t *testing.T) { | ||
| e := newTestEnv(t) | ||
| repo, _ := e.initRepoWithRemote("bc-dirty") | ||
| e.runGitm("repo", "add", repo, "--alias", "bc-dirty") | ||
|
|
||
| // Make repo dirty | ||
| e.writeFile(repo, "README.md", "# dirty\n") | ||
|
|
||
| r := e.runGitm("branch", "create", "feat/dirty-test", "--repo", "bc-dirty") | ||
| // Branch create on dirty repos should skip with a warning about uncommitted changes | ||
| e.assertExitCode(r, 0) | ||
| e.assertContains(r, "uncommitted changes") | ||
| } | ||
|
alexandreafj marked this conversation as resolved.
|
||
|
|
||
| func TestBranchRename_WithRepo(t *testing.T) { | ||
| e := newTestEnv(t) | ||
| repo, _ := e.initRepoWithRemote("br-repo") | ||
| e.runGitm("repo", "add", repo, "--alias", "br-repo") | ||
|
|
||
| // Create a branch to rename | ||
| e.mustGit(repo, "checkout", "-b", "old-name") | ||
| e.mustGit(repo, "push", "--set-upstream", "origin", "old-name") | ||
|
|
||
| r := e.runGitm("branch", "rename", "old-name", "new-name", "--repo", "br-repo") | ||
| e.assertExitCode(r, 0) | ||
|
|
||
| // Old branch should be gone, new should exist | ||
| if e.branchExists(repo, "old-name") { | ||
| t.Error("old branch still exists after rename") | ||
| } | ||
| if !e.branchExists(repo, "new-name") { | ||
| t.Error("new branch does not exist after rename") | ||
| } | ||
| } | ||
|
|
||
| func TestBranchRename_WithAll(t *testing.T) { | ||
| e := newTestEnv(t) | ||
| repo1, _ := e.initRepoWithRemote("br-all-1") | ||
| repo2, _ := e.initRepoWithRemote("br-all-2") | ||
| e.runGitm("repo", "add", repo1, "--alias", "br-all-1") | ||
| e.runGitm("repo", "add", repo2, "--alias", "br-all-2") | ||
|
|
||
| // Create the same branch in both repos | ||
| e.mustGit(repo1, "checkout", "-b", "shared-old") | ||
| e.mustGit(repo1, "push", "--set-upstream", "origin", "shared-old") | ||
| e.mustGit(repo2, "checkout", "-b", "shared-old") | ||
| e.mustGit(repo2, "push", "--set-upstream", "origin", "shared-old") | ||
|
|
||
| r := e.runGitm("branch", "rename", "shared-old", "shared-new", "--all") | ||
| e.assertExitCode(r, 0) | ||
|
|
||
| if e.branchExists(repo1, "shared-old") { | ||
| t.Error("old branch still exists in repo1") | ||
| } | ||
| if e.branchExists(repo2, "shared-old") { | ||
| t.Error("old branch still exists in repo2") | ||
| } | ||
| if !e.branchExists(repo1, "shared-new") { | ||
| t.Error("new branch missing in repo1") | ||
| } | ||
| if !e.branchExists(repo2, "shared-new") { | ||
| t.Error("new branch missing in repo2") | ||
| } | ||
| } | ||
|
|
||
| func TestBranchRename_NoRemote(t *testing.T) { | ||
| e := newTestEnv(t) | ||
| repo, origin := e.initRepoWithRemote("br-noremote") | ||
| e.runGitm("repo", "add", repo, "--alias", "br-noremote") | ||
|
|
||
| e.mustGit(repo, "checkout", "-b", "local-old") | ||
| e.mustGit(repo, "push", "--set-upstream", "origin", "local-old") | ||
|
|
||
| r := e.runGitm("branch", "rename", "local-old", "local-new", "--no-remote", "--repo", "br-noremote") | ||
| e.assertExitCode(r, 0) | ||
|
|
||
| // Local should be renamed | ||
| if e.branchExists(repo, "local-old") { | ||
| t.Error("old branch still exists locally") | ||
| } | ||
| if !e.branchExists(repo, "local-new") { | ||
| t.Error("new branch missing locally") | ||
| } | ||
|
|
||
| // Remote should still have old name (--no-remote skips remote ops) | ||
| remoteOut := e.mustGit(origin, "branch", "--list") | ||
| if !containsAny(remoteOut, "local-old") { | ||
| t.Log("Note: remote old branch was deleted even with --no-remote") | ||
| } | ||
| } | ||
|
|
||
| func TestBranchRename_NonExistentBranch(t *testing.T) { | ||
| e := newTestEnv(t) | ||
| repo, _ := e.initRepoWithRemote("br-ghost") | ||
| e.runGitm("repo", "add", repo, "--alias", "br-ghost") | ||
|
|
||
| r := e.runGitm("branch", "rename", "nonexistent-branch", "new", "--repo", "br-ghost") | ||
| // Renaming a branch that does not exist should fail | ||
| if r.ExitCode == 0 { | ||
| t.Fatalf("expected non-zero exit code when renaming a non-existent branch; stdout=%s stderr=%s", | ||
| r.Stdout, r.Stderr) | ||
| } | ||
| e.assertContains(r, "no registered repositories have a branch named") | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| package e2e | ||
|
|
||
| import ( | ||
| "path/filepath" | ||
| "testing" | ||
| ) | ||
|
|
||
| // ========================================================================== | ||
| // Phase 4: Checkout (gitm checkout) | ||
| // ========================================================================== | ||
|
|
||
| func TestCheckout_DefaultBranch_Master(t *testing.T) { | ||
| e := newTestEnv(t) | ||
| repo, _ := e.initRepoWithRemote("co-master") | ||
| e.runGitm("repo", "add", repo, "--alias", "co-master") | ||
|
|
||
| // Create and switch to a feature branch | ||
| e.mustGit(repo, "checkout", "-b", "feat/something") | ||
|
|
||
| r := e.runGitm("checkout", "master") | ||
| e.assertExitCode(r, 0) | ||
|
|
||
| // Should be back on the default branch (main in our test setup) | ||
| branch := e.currentBranch(repo) | ||
| if branch != "main" { | ||
| t.Errorf("expected to be on main (default), got %s", branch) | ||
| } | ||
| } | ||
|
|
||
| func TestCheckout_DefaultBranch_Main(t *testing.T) { | ||
| e := newTestEnv(t) | ||
| repo, _ := e.initRepoWithRemote("co-main") | ||
| e.runGitm("repo", "add", repo, "--alias", "co-main") | ||
|
|
||
| // Create and switch to a feature branch | ||
| e.mustGit(repo, "checkout", "-b", "feat/other") | ||
|
|
||
| r := e.runGitm("checkout", "main") | ||
| e.assertExitCode(r, 0) | ||
|
|
||
| branch := e.currentBranch(repo) | ||
| if branch != "main" { | ||
| t.Errorf("expected to be on main, got %s", branch) | ||
| } | ||
| } | ||
|
|
||
| func TestCheckout_ExistingBranch_WithRepo(t *testing.T) { | ||
| e := newTestEnv(t) | ||
| repo, _ := e.initRepoWithRemote("co-existing") | ||
| e.runGitm("repo", "add", repo, "--alias", "co-existing") | ||
|
|
||
| // Create a feature branch | ||
| e.mustGit(repo, "checkout", "-b", "feat/target") | ||
| e.mustGit(repo, "push", "--set-upstream", "origin", "feat/target") | ||
| e.mustGit(repo, "checkout", "main") | ||
|
|
||
| r := e.runGitm("checkout", "feat/target", "--repo", "co-existing") | ||
| e.assertExitCode(r, 0) | ||
|
|
||
| branch := e.currentBranch(repo) | ||
| if branch != "feat/target" { | ||
| t.Errorf("expected feat/target, got %s", branch) | ||
| } | ||
| } | ||
|
|
||
| func TestCheckout_NonExistentBranch(t *testing.T) { | ||
| e := newTestEnv(t) | ||
| repo, _ := e.initRepoWithRemote("co-ghost") | ||
| e.runGitm("repo", "add", repo, "--alias", "co-ghost") | ||
|
|
||
| r := e.runGitm("checkout", "branch-that-does-not-exist", "--repo", "co-ghost") | ||
| // Should succeed (exit 0) but skip the repo with a message | ||
| e.assertExitCode(r, 0) | ||
| // Should NOT have switched branches | ||
| branch := e.currentBranch(repo) | ||
| if branch != "main" { | ||
| t.Errorf("checkout of non-existent branch should not change current branch, but now on %s", branch) | ||
| } | ||
| } | ||
|
|
||
| func TestCheckout_DirtyRepo_Skips(t *testing.T) { | ||
| e := newTestEnv(t) | ||
| repo, _ := e.initRepoWithRemote("co-dirty") | ||
| e.runGitm("repo", "add", repo, "--alias", "co-dirty") | ||
|
|
||
| // Create target branch | ||
| e.mustGit(repo, "checkout", "-b", "feat/dirty-target") | ||
| e.mustGit(repo, "push", "--set-upstream", "origin", "feat/dirty-target") | ||
| e.mustGit(repo, "checkout", "main") | ||
|
|
||
| // Make repo dirty | ||
| e.writeFile(repo, "README.md", "# dirty content\n") | ||
|
|
||
| r := e.runGitm("checkout", "feat/dirty-target", "--repo", "co-dirty") | ||
| // Should skip with warning | ||
| e.assertExitCode(r, 0) | ||
|
|
||
| // Should still be on main (not switched) | ||
| branch := e.currentBranch(repo) | ||
| if branch != "main" { | ||
| t.Errorf("dirty repo should not switch branches, but now on %s", branch) | ||
| } | ||
| e.assertContains(r, "co-dirty") // Should mention the repo | ||
| } | ||
|
|
||
| func TestCheckout_UntrackedFiles_ShouldNotSkip(t *testing.T) { | ||
| e := newTestEnv(t) | ||
| repo, _ := e.initRepoWithRemote("co-untracked") | ||
| e.runGitm("repo", "add", repo, "--alias", "co-untracked") | ||
|
|
||
| // Create target branch | ||
| e.mustGit(repo, "checkout", "-b", "feat/untracked-test") | ||
| e.mustGit(repo, "push", "--set-upstream", "origin", "feat/untracked-test") | ||
| e.mustGit(repo, "checkout", "main") | ||
|
|
||
| // Add untracked file only (should NOT make repo dirty per docs) | ||
| e.writeFile(repo, "untracked-new.txt", "I am untracked\n") | ||
|
|
||
| r := e.runGitm("checkout", "feat/untracked-test", "--repo", "co-untracked") | ||
| e.assertExitCode(r, 0) | ||
|
|
||
| // Per docs: untracked files are ignored — checkout should proceed | ||
| branch := e.currentBranch(repo) | ||
| if branch != "feat/untracked-test" { | ||
| t.Errorf("untracked files should not block checkout, but stayed on %s", branch) | ||
| } | ||
| } | ||
|
|
||
| func TestCheckout_RemoteOnlyBranch(t *testing.T) { | ||
| e := newTestEnv(t) | ||
| repo, origin := e.initRepoWithRemote("co-remote") | ||
| e.runGitm("repo", "add", repo, "--alias", "co-remote") | ||
|
|
||
| // Create a branch on remote only (via another clone) | ||
| other := e.cloneRepo(origin, "co-remote-other") | ||
| e.mustGit(other, "checkout", "-b", "feat/remote-only") | ||
| e.writeFile(other, "remote.txt", "from remote\n") | ||
| e.mustGit(other, "add", ".") | ||
| e.mustGit(other, "commit", "-m", "remote commit") | ||
| e.mustGit(other, "push", "--set-upstream", "origin", "feat/remote-only") | ||
|
|
||
| // Our repo doesn't know about this branch locally | ||
| r := e.runGitm("checkout", "feat/remote-only", "--repo", "co-remote") | ||
| e.assertExitCode(r, 0) | ||
|
|
||
| branch := e.currentBranch(repo) | ||
| if branch != "feat/remote-only" { | ||
| t.Fatalf("expected checkout of remote-only branch to switch to %q, got %q\nstdout: %s\nstderr: %s", | ||
| "feat/remote-only", branch, r.Stdout, r.Stderr) | ||
| } | ||
| } | ||
|
|
||
| func TestCheckout_PullsAfterSwitch(t *testing.T) { | ||
| e := newTestEnv(t) | ||
| repo, origin := e.initRepoWithRemote("co-pulls") | ||
| e.runGitm("repo", "add", repo, "--alias", "co-pulls") | ||
|
|
||
| // Create a branch, push it, then push more commits from another clone | ||
| e.mustGit(repo, "checkout", "-b", "feat/pull-test") | ||
| e.writeFile(repo, "first.txt", "first\n") | ||
| e.mustGit(repo, "add", ".") | ||
| e.mustGit(repo, "commit", "-m", "first") | ||
| e.mustGit(repo, "push", "--set-upstream", "origin", "feat/pull-test") | ||
| e.mustGit(repo, "checkout", "main") | ||
|
|
||
| // Push more commits from another clone | ||
| other := e.cloneRepo(origin, "co-pulls-other") | ||
| e.mustGit(other, "checkout", "feat/pull-test") | ||
| e.writeFile(other, "second.txt", "second\n") | ||
| e.mustGit(other, "add", ".") | ||
| e.mustGit(other, "commit", "-m", "second from other") | ||
| e.mustGit(other, "push") | ||
|
|
||
| // Checkout should switch AND pull | ||
| r := e.runGitm("checkout", "feat/pull-test", "--repo", "co-pulls") | ||
| e.assertExitCode(r, 0) | ||
|
|
||
| // Should have the latest file from the other clone | ||
| if !e.fileExists(filepath.Join(repo, "second.txt")) { | ||
| t.Error("checkout did not pull latest — second.txt missing") | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.