This commit is contained in:
2026-01-21 11:27:51 -05:00
commit f70af9b185
50 changed files with 17166 additions and 0 deletions
+288
View File
@@ -0,0 +1,288 @@
(ns lazygitclj.core-test
"Unit tests for lazygitclj.core namespace - model and update functions"
(:require [clojure.test :refer [deftest testing is]]
[lazygitclj.core :as core]
[tui.simple :as tui]))
;; Helper to create key messages in the format the TUI library uses
(defn key-msg [k]
(cond
(char? k) [:key {:char k}]
(keyword? k) [:key k]
(and (vector? k) (= :ctrl (first k))) [:key {:ctrl true :char (second k)}]
:else [:key k]))
;; === Model Tests ===
(deftest test-file-items
(testing "combines staged and unstaged files"
(let [model {:staged [{:path "a.txt"} {:path "b.txt"}]
:unstaged [{:path "c.txt"}]}
items (core/file-items model)]
(is (= 3 (count items)))
(is (= :staged (:type (first items))))
(is (= :unstaged (:type (last items))))))
(testing "returns empty vector when no files"
(let [model {:staged [] :unstaged []}]
(is (= [] (core/file-items model))))))
(deftest test-current-items
(testing "returns file items for files panel"
(let [model {:panel :files
:staged [{:path "a.txt"}]
:unstaged [{:path "b.txt"}]}]
(is (= 2 (count (core/current-items model))))))
(testing "returns commits for commits panel"
(let [model {:panel :commits
:commits-tab :commits
:commits [{:sha "abc123"}]
:reflog []}]
(is (= 1 (count (core/current-items model))))))
(testing "returns reflog for commits panel with reflog tab"
(let [model {:panel :commits
:commits-tab :reflog
:commits []
:reflog [{:sha "def456"} {:sha "ghi789"}]}]
(is (= 2 (count (core/current-items model))))))
(testing "returns branches for branches panel"
(let [model {:panel :branches
:branches-tab :local
:branches ["main" "feature"]}]
(is (= 2 (count (core/current-items model))))))
(testing "returns remote branches for branches panel with remotes tab"
(let [model {:panel :branches
:branches-tab :remotes
:remote-branches ["origin/main"]}]
(is (= 1 (count (core/current-items model))))))
(testing "returns tags for branches panel with tags tab"
(let [model {:panel :branches
:branches-tab :tags
:tags ["v1.0.0" "v2.0.0"]}]
(is (= 2 (count (core/current-items model))))))
(testing "returns stashes for stash panel"
(let [model {:panel :stash
:stashes [{:ref "stash@{0}"}]}]
(is (= 1 (count (core/current-items model)))))))
(deftest test-clamp-cursor
(testing "clamps cursor to valid range"
(let [model {:panel :files
:cursor 10
:staged [{:path "a.txt"}]
:unstaged []}
clamped (core/clamp-cursor model)]
(is (= 0 (:cursor clamped)))))
(testing "keeps cursor at valid position"
(let [model {:panel :files
:cursor 1
:staged [{:path "a.txt"} {:path "b.txt"}]
:unstaged []}
clamped (core/clamp-cursor model)]
(is (= 1 (:cursor clamped)))))
(testing "handles empty lists"
(let [model {:panel :files
:cursor 5
:staged []
:unstaged []}
clamped (core/clamp-cursor model)]
(is (= 0 (:cursor clamped))))))
(deftest test-truncate
(testing "truncates long strings"
(is (= "hello..." (core/truncate "hello world" 5))))
(testing "keeps short strings"
(is (= "hi" (core/truncate "hi" 10))))
(testing "handles nil"
(is (nil? (core/truncate nil 10)))))
;; === Update Tests ===
(deftest test-update-model-quit
(testing "q returns quit command"
(let [[model cmd] (core/update-model {} (key-msg \q))]
(is (= [:quit] cmd))))
(testing "ctrl-c returns quit command"
(let [[model cmd] (core/update-model {} [:key {:ctrl true :char \c}])]
(is (= [:quit] cmd)))))
(deftest test-update-model-panel-switch
(testing "number keys switch panels (2-5 matching lazygit)"
(let [[model _] (core/update-model {:panel :commits :cursor 0
:staged [] :unstaged []} (key-msg \2))]
(is (= :files (:panel model))))
(let [[model _] (core/update-model {:panel :files :cursor 0
:branches-tab :local
:branches [] :remote-branches []
:tags []} (key-msg \3))]
(is (= :branches (:panel model))))
(let [[model _] (core/update-model {:panel :files :cursor 0
:commits-tab :commits
:commits [] :reflog []} (key-msg \4))]
(is (= :commits (:panel model))))
(let [[model _] (core/update-model {:panel :files :cursor 0
:stashes []} (key-msg \5))]
(is (= :stash (:panel model)))))
(testing "l key cycles panels right (files → branches → commits → stash → files)"
(let [[model _] (core/update-model {:panel :files :cursor 0
:branches-tab :local
:branches [] :remote-branches []
:tags []} (key-msg \l))]
(is (= :branches (:panel model))))
(let [[model _] (core/update-model {:panel :branches :cursor 0
:commits-tab :commits
:commits [] :reflog []} (key-msg \l))]
(is (= :commits (:panel model))))
(let [[model _] (core/update-model {:panel :commits :cursor 0
:stashes []} (key-msg \l))]
(is (= :stash (:panel model))))
(let [[model _] (core/update-model {:panel :stash :cursor 0
:staged [] :unstaged []} (key-msg \l))]
(is (= :files (:panel model))))))
(deftest test-update-model-cursor-movement
(testing "j moves cursor down"
(let [[model _] (core/update-model {:panel :files :cursor 0
:staged [{:path "a"} {:path "b"}]
:unstaged []} (key-msg \j))]
(is (= 1 (:cursor model)))))
(testing "k moves cursor up"
(let [[model _] (core/update-model {:panel :files :cursor 1
:staged [{:path "a"} {:path "b"}]
:unstaged []} (key-msg \k))]
(is (= 0 (:cursor model)))))
(testing "down arrow moves cursor down"
(let [[model _] (core/update-model {:panel :files :cursor 0
:staged [{:path "a"} {:path "b"}]
:unstaged []} (key-msg :down))]
(is (= 1 (:cursor model)))))
(testing "up arrow moves cursor up"
(let [[model _] (core/update-model {:panel :files :cursor 1
:staged [{:path "a"} {:path "b"}]
:unstaged []} (key-msg :up))]
(is (= 0 (:cursor model))))))
(deftest test-update-model-help-menu
(testing "? opens help menu"
(let [[model _] (core/update-model {:menu-mode nil} (key-msg \?))]
(is (= :help (:menu-mode model))))))
(deftest test-update-model-reset-menu
(testing "D opens reset options menu"
(let [[model _] (core/update-model {:menu-mode nil} (key-msg \D))]
(is (= :reset-options (:menu-mode model))))))
(deftest test-update-model-input-mode
(testing "c in files panel opens commit input when staged files exist"
(let [[model _] (core/update-model {:panel :files
:staged [{:path "a.txt"}]
:unstaged []} (key-msg \c))]
(is (= :commit (:input-mode model)))))
(testing "c in files panel shows message when no staged files"
(let [[model _] (core/update-model {:panel :files
:staged []
:unstaged []} (key-msg \c))]
(is (= "Nothing staged to commit" (:message model))))))
(deftest test-update-input-mode
(testing "escape cancels input mode"
(let [[model _] (core/update-input-mode {:input-mode :commit
:input-buffer "test"
:input-context nil} (key-msg :escape))]
(is (nil? (:input-mode model)))
(is (= "" (:input-buffer model)))))
(testing "backspace removes last character"
(let [[model _] (core/update-input-mode {:input-mode :commit
:input-buffer "abc"
:input-context nil} (key-msg :backspace))]
(is (= "ab" (:input-buffer model))))))
(deftest test-update-commits-tabs
(testing "] switches to reflog tab in commits panel"
(let [[model _] (core/update-model {:panel :commits
:commits-tab :commits
:cursor 0
:commits []
:reflog []} (key-msg \]))]
(is (= :reflog (:commits-tab model)))))
(testing "[ switches to commits tab in commits panel"
(let [[model _] (core/update-model {:panel :commits
:commits-tab :reflog
:cursor 0
:commits []
:reflog []} (key-msg \[))]
(is (= :commits (:commits-tab model))))))
(deftest test-update-branches-tabs
(testing "] cycles branches tabs forward"
(let [[model _] (core/update-branches {:branches-tab :local
:cursor 0
:branches []} (key-msg \]))]
(is (= :remotes (:branches-tab model))))
(let [[model _] (core/update-branches {:branches-tab :remotes
:cursor 0
:remote-branches []} (key-msg \]))]
(is (= :tags (:branches-tab model))))
(let [[model _] (core/update-branches {:branches-tab :tags
:cursor 0
:tags []} (key-msg \]))]
(is (= :local (:branches-tab model)))))
(testing "[ cycles branches tabs backward"
(let [[model _] (core/update-branches {:branches-tab :local
:cursor 0
:tags []} (key-msg \[))]
(is (= :tags (:branches-tab model))))))
(deftest test-update-stash-menu
(testing "escape closes stash menu"
(let [[model _] (core/update-stash-menu {:menu-mode :stash-options} (key-msg :escape))]
(is (nil? (:menu-mode model))))))
(deftest test-update-reset-menu
(testing "escape closes reset menu"
(let [[model _] (core/update-reset-menu {:menu-mode :reset-options} (key-msg :escape))]
(is (nil? (:menu-mode model))))))
(deftest test-update-help
(testing "escape closes help"
(let [[model _] (core/update-help {:menu-mode :help} (key-msg :escape))]
(is (nil? (:menu-mode model)))))
(testing "q closes help"
(let [[model _] (core/update-help {:menu-mode :help} (key-msg \q))]
(is (nil? (:menu-mode model)))))
(testing "? closes help"
(let [[model _] (core/update-help {:menu-mode :help} (key-msg \?))]
(is (nil? (:menu-mode model))))))
;; Run tests when executed directly
(defn -main [& args]
(clojure.test/run-tests 'lazygitclj.core-test))
+323
View File
@@ -0,0 +1,323 @@
(ns lazygitclj.git-test
"Unit tests for lazygitclj.git namespace"
(:require [clojure.test :refer [deftest testing is use-fixtures]]
[lazygitclj.git :as git]
[clojure.string :as str]
[babashka.process :refer [shell]]))
;; === Test Fixtures ===
(def ^:dynamic *test-repo* nil)
(defn- sh [& args]
(try
(-> (apply shell {:out :string :err :string :dir *test-repo*} args)
:out
str/trim-newline)
(catch Exception _ nil)))
(defn create-test-repo []
(let [repo-dir (str "/tmp/lazygitclj-unit-test-" (System/currentTimeMillis))]
(shell "mkdir" "-p" repo-dir)
(shell {:dir repo-dir} "git" "init" "-b" "main")
(shell {:dir repo-dir} "git" "config" "user.email" "test@example.com")
(shell {:dir repo-dir} "git" "config" "user.name" "Test User")
;; Create initial files
(spit (str repo-dir "/README.md") "# Test Project\n")
(spit (str repo-dir "/file1.txt") "line1\n")
(shell {:dir repo-dir} "git" "add" ".")
(shell {:dir repo-dir} "git" "commit" "-m" "Initial commit")
repo-dir))
(defn delete-test-repo [repo-dir]
(shell "rm" "-rf" repo-dir))
(defn with-test-repo [f]
(let [repo-dir (create-test-repo)]
(try
(binding [*test-repo* repo-dir]
;; Change to test repo for git commands
(let [original-dir (System/getProperty "user.dir")]
(System/setProperty "user.dir" repo-dir)
(try
(f)
(finally
(System/setProperty "user.dir" original-dir)))))
(finally
(delete-test-repo repo-dir)))))
(use-fixtures :each with-test-repo)
;; === Status Tests ===
(deftest test-current-branch
(testing "returns current branch name"
(is (= "main" (sh "git" "branch" "--show-current")))))
(deftest test-head-sha
(testing "returns short SHA of HEAD"
(let [sha (sh "git" "rev-parse" "--short" "HEAD")]
(is (string? sha))
(is (>= (count sha) 7)))))
(deftest test-repo-root
(testing "returns repo root directory"
(let [root (sh "git" "rev-parse" "--show-toplevel")]
(is (string? root))
(is (str/starts-with? root "/tmp/lazygitclj-unit-test-")))))
;; === File Status Tests ===
(deftest test-staged-files
(testing "returns empty list when nothing staged"
;; Modify file but don't stage
(spit (str *test-repo* "/file1.txt") "modified\n")
(let [staged (git/staged-files)]
(is (empty? staged))))
(testing "returns staged files after git add"
(spit (str *test-repo* "/file1.txt") "modified\n")
(sh "git" "add" "file1.txt")
(let [staged (git/staged-files)]
(is (= 1 (count staged)))
(is (= "file1.txt" (:path (first staged)))))))
(deftest test-unstaged-files
(testing "returns modified unstaged files"
(spit (str *test-repo* "/file1.txt") "modified\n")
(let [unstaged (git/unstaged-files)]
(is (= 1 (count unstaged)))
(is (= "file1.txt" (:path (first unstaged))))))
(testing "returns untracked files"
(spit (str *test-repo* "/newfile.txt") "new content\n")
(let [unstaged (git/unstaged-files)]
(is (some #(= "newfile.txt" (:path %)) unstaged)))))
;; === Branch Tests ===
(deftest test-branches
(testing "returns list of local branches"
(let [branches (git/branches)]
(is (vector? branches))
(is (some #(= "main" %) branches))))
(testing "includes newly created branches"
(sh "git" "branch" "feature-branch")
(let [branches (git/branches)]
(is (some #(= "feature-branch" %) branches)))))
(deftest test-create-branch
(testing "creates a new branch and switches to it"
(git/create-branch "new-feature")
(is (= "new-feature" (sh "git" "branch" "--show-current")))))
(deftest test-checkout-branch
(testing "switches to existing branch"
(sh "git" "branch" "other-branch")
(git/checkout-branch "other-branch")
(is (= "other-branch" (sh "git" "branch" "--show-current")))))
(deftest test-delete-branch
(testing "deletes a branch"
(sh "git" "branch" "to-delete")
(git/delete-branch "to-delete")
(let [branches (git/branches)]
(is (not (some #(= "to-delete" %) branches))))))
(deftest test-rename-branch
(testing "renames a branch"
(sh "git" "branch" "old-name")
(git/rename-branch "old-name" "new-name")
(let [branches (git/branches)]
(is (some #(= "new-name" %) branches))
(is (not (some #(= "old-name" %) branches))))))
;; === Staging Tests ===
(deftest test-stage-file
(testing "stages a modified file"
(spit (str *test-repo* "/file1.txt") "modified\n")
(git/stage-file "file1.txt")
(let [staged (git/staged-files)]
(is (= 1 (count staged))))))
(deftest test-unstage-file
(testing "unstages a staged file"
(spit (str *test-repo* "/file1.txt") "modified\n")
(sh "git" "add" "file1.txt")
(git/unstage-file "file1.txt")
(let [staged (git/staged-files)]
(is (empty? staged)))))
(deftest test-stage-all
(testing "stages all changed files"
(spit (str *test-repo* "/file1.txt") "modified\n")
(spit (str *test-repo* "/newfile.txt") "new\n")
(git/stage-all)
(let [staged (git/staged-files)]
(is (= 2 (count staged))))))
;; === Commit Tests ===
(deftest test-commit
(testing "creates a commit with message"
(spit (str *test-repo* "/file1.txt") "modified\n")
(sh "git" "add" ".")
(git/commit "Test commit message")
(let [log (sh "git" "log" "--oneline" "-1")]
(is (str/includes? log "Test commit message")))))
(deftest test-recent-commits
(testing "returns recent commits"
(let [commits (git/recent-commits 5)]
(is (seq commits))
(is (= "Initial commit" (:subject (first commits)))))))
;; === Stash Tests ===
(deftest test-stash-all
(testing "stashes all changes"
(spit (str *test-repo* "/file1.txt") "modified\n")
(git/stash-all)
(let [stashes (git/stash-list)]
(is (= 1 (count stashes))))
;; Working tree should be clean
(let [unstaged (git/unstaged-files)]
(is (empty? unstaged)))))
(deftest test-stash-pop
(testing "pops stash and removes it from list"
(spit (str *test-repo* "/file1.txt") "modified\n")
(git/stash-all)
(git/stash-pop)
(let [stashes (git/stash-list)
unstaged (git/unstaged-files)]
(is (empty? stashes))
(is (= 1 (count unstaged))))))
(deftest test-stash-apply
(testing "applies stash but keeps it in list"
(spit (str *test-repo* "/file1.txt") "modified\n")
(git/stash-all)
(git/stash-apply)
(let [stashes (git/stash-list)
unstaged (git/unstaged-files)]
(is (= 1 (count stashes)))
(is (= 1 (count unstaged))))))
(deftest test-stash-drop
(testing "drops stash from list"
(spit (str *test-repo* "/file1.txt") "modified\n")
(git/stash-all)
(git/stash-drop)
(let [stashes (git/stash-list)]
(is (empty? stashes)))))
;; === Reset Tests ===
(deftest test-discard-file
(testing "discards unstaged changes to a file"
(spit (str *test-repo* "/file1.txt") "modified\n")
(git/discard-file "file1.txt")
(let [content (slurp (str *test-repo* "/file1.txt"))]
(is (= "line1\n" content)))))
(deftest test-discard-all-unstaged
(testing "discards all unstaged changes"
(spit (str *test-repo* "/file1.txt") "modified\n")
(git/discard-all-unstaged)
(let [unstaged (git/unstaged-files)]
(is (empty? unstaged)))))
(deftest test-reset-staged
(testing "unstages all staged files"
(spit (str *test-repo* "/file1.txt") "modified\n")
(sh "git" "add" ".")
(git/reset-staged)
(let [staged (git/staged-files)]
(is (empty? staged)))))
;; === Tag Tests ===
(deftest test-list-tags
(testing "returns empty list when no tags"
(let [tags (git/list-tags)]
(is (empty? tags))))
(testing "returns tags after creating one"
(sh "git" "tag" "v1.0.0")
(let [tags (git/list-tags)]
(is (some #(= "v1.0.0" %) tags)))))
(deftest test-create-tag
(testing "creates a tag"
(git/create-tag "v2.0.0")
(let [tags (git/list-tags)]
(is (some #(= "v2.0.0" %) tags)))))
(deftest test-delete-tag
(testing "deletes a tag"
(sh "git" "tag" "to-delete")
(git/delete-tag "to-delete")
(let [tags (git/list-tags)]
(is (not (some #(= "to-delete" %) tags))))))
;; === Reflog Tests ===
(deftest test-reflog
(testing "returns reflog entries"
(let [reflog (git/reflog 10)]
(is (seq reflog))
(is (every? :sha reflog))
(is (every? :ref reflog)))))
;; === Cherry-pick and Revert Tests ===
(deftest test-cherry-pick-commit
(testing "cherry-picks a commit"
;; Create a branch with a commit
(sh "git" "checkout" "-b" "feature")
(spit (str *test-repo* "/feature.txt") "feature content\n")
(sh "git" "add" ".")
(sh "git" "commit" "-m" "Feature commit")
(let [feature-sha (sh "git" "rev-parse" "--short" "HEAD")]
;; Go back to main
(sh "git" "checkout" "main")
;; Cherry-pick
(git/cherry-pick-commit feature-sha)
;; Verify file exists
(is (.exists (java.io.File. (str *test-repo* "/feature.txt")))))))
(deftest test-revert-commit
(testing "reverts a commit"
;; Make a change and commit
(spit (str *test-repo* "/file1.txt") "modified\n")
(sh "git" "add" ".")
(sh "git" "commit" "-m" "Change to revert")
(let [sha (sh "git" "rev-parse" "--short" "HEAD")]
;; Revert it
(git/revert-commit sha)
;; Verify content is back to original
(let [content (slurp (str *test-repo* "/file1.txt"))]
(is (= "line1\n" content))))))
;; === Merge Tests ===
(deftest test-merge-branch
(testing "merges a branch into current"
;; Create feature branch with changes
(sh "git" "checkout" "-b" "to-merge")
(spit (str *test-repo* "/merged.txt") "merged content\n")
(sh "git" "add" ".")
(sh "git" "commit" "-m" "Merge commit")
;; Go back to main
(sh "git" "checkout" "main")
;; Merge
(git/merge-branch "to-merge")
;; Verify file exists on main
(is (.exists (java.io.File. (str *test-repo* "/merged.txt"))))))
;; Run tests when executed directly
(defn -main [& args]
(clojure.test/run-tests 'lazygitclj.git-test))