diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c1b374c3..95d1ac46 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,28 +13,41 @@ jobs: strategy: matrix: release: [stable, nightly] + env: + CI: "1" steps: - uses: actions/checkout@v4 - - name: Install Dependencies - run: | - mkdir -p ~/.local/share/neogit-test/site/pack/plenary.nvim/start - cd ~/.local/share/neogit-test/site/pack/plenary.nvim/start - git clone https://github.com/nvim-lua/plenary.nvim - - mkdir -p ~/.local/share/neogit-test/site/pack/telescope.nvim/start - cd ~/.local/share/neogit-test/site/pack/telescope.nvim/start - git clone https://github.com/nvim-telescope/telescope.nvim - - name: Install Neovim run: | wget https://github.com/neovim/neovim/releases/download/${{ matrix.release }}/nvim-linux64.tar.gz tar -zxf nvim-linux64.tar.gz sudo ln -s $(pwd)/nvim-linux64/bin/nvim /usr/local/bin - - name: Test - continue-on-error: false - env: - ci: "1" + - name: Install Dependencies run: | - make test + mkdir -p ~/.local/share/nvim/site/pack/ + cd ~/.local/share/nvim/site/pack/ + + mkdir -p ./plenary.nvim/start + cd ./plenary.nvim/start + git clone https://github.com/nvim-lua/plenary.nvim + + mkdir -p ./telescope.nvim/start + cd ./telescope.nvim/start + git clone https://github.com/nvim-telescope/telescope.nvim + + # - name: Luassert Test + # continue-on-error: true + # run: | + # make test + + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: E2E Test + env: + NVIM_RUBY_LOG_LEVEL: "debug" + run: | + bundle exec rspec diff --git a/.gitignore b/.gitignore index 6bfce671..5e39200e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ doc/tags # Environment .envrc +/tests/.min/* diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..15a27998 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.3.0 diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..da9e0f36 --- /dev/null +++ b/Gemfile @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +ruby File.read(".ruby-version").strip + +source "https://rubygems.org" + +gem "debug" +gem "fuubar" +gem "neovim" +gem "quickfix_formatter" +gem "rspec" +gem "tmpdir" +gem "git" +gem "super_diff" +gem "activesupport" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..95b6b8e2 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,106 @@ +GEM + remote: https://rubygems.org/ + specs: + activesupport (7.1.2) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) + attr_extras (7.1.0) + base64 (0.2.0) + bigdecimal (3.1.5) + concurrent-ruby (1.2.2) + connection_pool (2.4.1) + debug (1.9.1) + irb (~> 1.10) + reline (>= 0.3.8) + diff-lcs (1.5.0) + drb (2.2.0) + ruby2_keywords + fileutils (1.7.2) + fuubar (2.5.1) + rspec-core (~> 3.0) + ruby-progressbar (~> 1.4) + git (1.19.0) + addressable (~> 2.8) + rchardet (~> 1.8) + i18n (1.14.1) + concurrent-ruby (~> 1.0) + io-console (0.7.1) + irb (1.11.0) + rdoc + reline (>= 0.3.8) + minitest (5.20.0) + msgpack (1.7.2) + multi_json (1.15.0) + mutex_m (0.2.0) + neovim (0.9.1) + msgpack (~> 1.1) + multi_json (~> 1.0) + optimist (3.1.0) + patience_diff (1.2.0) + optimist (~> 3.0) + psych (5.1.2) + stringio + public_suffix (5.0.4) + quickfix_formatter (0.1.0) + rspec (>= 3.12.0) + rchardet (1.8.0) + rdoc (6.6.2) + psych (>= 4.0.0) + reline (0.4.1) + io-console (~> 0.5) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) + rspec-core (3.12.2) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.6) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-support (3.12.1) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + stringio (3.1.0) + super_diff (0.10.0) + attr_extras (>= 6.2.4) + diff-lcs + patience_diff + tmpdir (0.2.0) + fileutils + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + +PLATFORMS + arm64-darwin-22 + arm64-darwin-23 + x86_64-darwin-20 + x86_64-linux + +DEPENDENCIES + activesupport + debug + fuubar + git + neovim + quickfix_formatter + rspec + super_diff + tmpdir + +RUBY VERSION + ruby 3.3.0p0 + +BUNDLED WITH + 2.4.21 diff --git a/Makefile b/Makefile index eba24cd0..06663f0a 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ test: - LUA_PATH="./?.lua" TEST_FILES=$$TEST_FILES NEOGIT_LOG_LEVEL=error NEOGIT_LOG_CONSOLE="sync" GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null NVIM_APPNAME=neogit-test nvim --headless -S "./tests/init.lua" + TEMP_DIR=$$TEMP_DIR TEST_FILES=$$TEST_FILES NEOGIT_LOG_LEVEL=error NEOGIT_LOG_CONSOLE="sync" GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null NVIM_APPNAME=neogit-test nvim --headless -S "./tests/init.lua" lint: selene --config selene/config.toml lua diff --git a/spec/popups/branch_popup_spec.rb b/spec/popups/branch_popup_spec.rb new file mode 100644 index 00000000..ba26b9a2 --- /dev/null +++ b/spec/popups/branch_popup_spec.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "Branch Popup", :git, :nvim do + describe "Variables" do + describe "branch..description" do + it "can edit branch description" + end + + describe "branch..{merge,remote}" do + it "can set the upstream for current branch" + end + + describe "branch..rebase" do + it "can change rebase setting" + end + + describe "branch..pushRemote" do + it "can change pushRemote for current branch" + end + end + + describe "Actions" do + describe "Checkout branch/revision" do + it "can checkout a local branch" + it "can checkout a remote branch" + it "can checkout a tag" + it "can checkout HEAD" + it "can checkout a commit" + end + + describe "Checkout local branch" do + before { git.branch("new-local-branch").checkout } + + it "can checkout a local branch" do + nvim.feedkeys("bl") + nvim.feedkeys("master") + expect(git.current_branch).to eq "master" + end + + it "creates and checks out a new local branch when choosing a remote" + end + + describe "Checkout recent branch" do + it "can checkout a local branch" + end + + describe "Checkout new branch" do + it "can create and checkout a branch" do + nvim.input("new-branch") + nvim.feedkeys("bc") + nvim.feedkeys("master") + + expect(git.current_branch).to eq "new-branch" + end + + it "replaces spaces with dashes in user input" do + nvim.input("new branch with spaces") + nvim.feedkeys("bc") + nvim.feedkeys("master") + + expect(git.current_branch).to eq "new-branch-with-spaces" + end + + it "lets you pick a base branch" do + git.branch("new-base-branch").checkout + + nvim.input("feature-branch") + nvim.feedkeys("bc") + nvim.feedkeys("master") + + expect(git.current_branch).to eq "feature-branch" + + expect( + git.merge_base("feature-branch", "master").first.sha + ).to eq(git.revparse("master")) + end + end + end + + describe "Checkout new spin-off" do + it "can create and checkout a spin-off branch" + end + + describe "Checkout new worktree" do + it "can create and checkout a worktree" + end + + describe "Create new branch" do + it "can create a new branch" + end + + describe "Create new spin-off" do + it "can create a new spin-off" + + context "when there are uncommitted changes" do + it "checks out the spun-off branch" + end + end + + describe "Create new worktree" do + it "can create a new worktree" + end + + describe "Configure" do + it "Launches the configuration popup" + end + + describe "Rename" do + it "can rename a branch" + end + + describe "reset" do + it "can reset a branch" + end + + describe "delete" do + it "can delete a branch" + end + + describe "pull request" do + # Requires Neovim 0.10 + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..979e9d77 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require "tmpdir" +require "git" +require "neovim" +require "debug" +require "active_support/all" + +PROJECT_DIR = File.expand_path(File.join(__dir__, "..")) + +Dir[File.join(File.expand_path("."), "spec", "support", "**", "*.rb")].each { |f| require f } + +# Thread.new do +# loop do +# sleep 10 # seconds +# puts "=" * 80; +# Thread.list.each.with_index { |t, i| puts "== Thread #{i}"; puts t.backtrace } +# end +# end + +# module Neovim +# class Connection +# def self.child(argv) +# argv = argv.include?("--embed") ? argv : argv + ["--embed"] +# +# io = ::IO.popen(argv, "rb+") +# # Process.detach(io.pid) +# +# new(io, io) +# end +# end +# end + +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + + config.shared_context_metadata_behavior = :apply_to_host_groups + config.filter_run_when_matching :focus + config.example_status_persistence_file_path = "spec/examples.txt" + config.disable_monkey_patching! + config.warnings = true + config.profile_examples = 10 + config.order = :random + + config.include Helpers + + config.around(:each) do |example| + Dir.mktmpdir do |tmp| + Dir.chdir(tmp) do + Git.init + example.run + end + end + end +end diff --git a/spec/support/context/git.rb b/spec/support/context/git.rb new file mode 100644 index 00000000..a3191723 --- /dev/null +++ b/spec/support/context/git.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +RSpec.shared_context "git", :git do + let(:git) { Git.open(Dir.pwd) } + + before do + system("touch testfile") + + git.config("user.email", "test@example.com") + git.config("user.name", "tester") + git.add("testfile") + git.commit("Initial commit") + end +end diff --git a/spec/support/context/nvim.rb b/spec/support/context/nvim.rb new file mode 100644 index 00000000..4dd80823 --- /dev/null +++ b/spec/support/context/nvim.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +RSpec.shared_context "nvim", :nvim do + let(:nvim) { NeovimClient.new } + + before { nvim.setup } + after { nvim.teardown } +end diff --git a/spec/support/dependencies.rb b/spec/support/dependencies.rb new file mode 100644 index 00000000..491ec299 --- /dev/null +++ b/spec/support/dependencies.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +return if ENV["CI"] + +def dir_name(name) + name.match(/[^\/]+\/(?[^\.]+)/)[:dir_name] +end + +def ensure_installed(name) + tmp = File.join(PROJECT_DIR, "tmp") + Dir.mkdir(tmp) if !Dir.exist?(tmp) + + dir = File.join(tmp, dir_name(name)) + + return if Dir.exist?(dir) && !Dir.empty?(dir) + + puts "Downloading dependency #{name} to #{dir}" + Dir.mkdir(dir) + Git.clone("git@github.com:#{name}.git", dir) +end + +ensure_installed "nvim-lua/plenary.nvim" +ensure_installed "nvim-telescope/telescope.nvim" diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb new file mode 100644 index 00000000..b7bf07fd --- /dev/null +++ b/spec/support/helpers.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Helpers + # def wait_for_expect + # last_error = nil + # success = false + # + # 5.times do + # begin + # yield + # success = true + # break + # rescue RSpec::Expectations::ExpectationNotMetError => e + # last_error = e + # sleep 0.5 + # end + # end + # + # raise last_error if !success && last_error + # end +end diff --git a/spec/support/init.lua b/spec/support/init.lua new file mode 100644 index 00000000..e69de29b diff --git a/spec/support/neovim_client.rb b/spec/support/neovim_client.rb new file mode 100644 index 00000000..26615d50 --- /dev/null +++ b/spec/support/neovim_client.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +class NeovimClient + def initialize + @instance = nil + end + + def setup + @instance = attach_child + + if ENV["CI"] + lua <<~LUA + vim.cmd.runtime("plugin/plenary.vim") + vim.cmd.runtime("plugin/neogit.lua") + LUA + else + # Sets up the runtimepath + runtime_dependencies.each do |dep| + lua "vim.opt.runtimepath:prepend('#{dep}')" + end + end + + lua "vim.opt.runtimepath:prepend('#{PROJECT_DIR}')" + + lua <<~LUA + require("plenary") + require('neogit').setup() + require('neogit').open() + LUA + + sleep(0.025) # Seems to be about right + end + + def teardown + @instance.shutdown + @instance = nil + end + + def print_screen + puts get_lines + end + + def lua(code) + @instance.exec_lua(code, []) + end + + def get_lines + @instance.current.buffer.get_lines(0, -1, true).join("\n") + end + + # Overload vim.fn.input() to prevent blocking. + def input(*args) + lua <<~LUA + local inputs = { #{args.map(&:inspect).join(",")} } + + vim.fn.input = function() + return table.remove(inputs, 1) + end + LUA + end + + # Higher-level user input + def feedkeys(keys, mode: 'm') + @instance.feedkeys( + @instance.replace_termcodes(keys, true, false, true), + mode, + false + ) + end + + def attach_child + if ENV["CI"] + Neovim.attach_child(["nvim", "--embed", "--headless"]) + else + Neovim.attach_child(["nvim", "--embed", "--clean", "--headless"]) + end + end + + def runtime_dependencies + Dir[File.join(PROJECT_DIR, "tmp", "*")].select { Dir.exist? _1 } + end +end diff --git a/tests/init.lua b/tests/init.lua index e6002ca3..fdfb1c8d 100644 --- a/tests/init.lua +++ b/tests/init.lua @@ -1,25 +1,11 @@ local util = require("tests.util.util") -local function ensure_installed(repo) - local name = repo:match(".+/(.+)$") - - local install_path = util.neogit_test_base_dir .. name - - vim.opt.runtimepath:prepend(install_path) - - if not vim.loop.fs_stat(install_path) then - print("* Downloading " .. name .. " to '" .. install_path .. "/'") - vim.fn.system { "git", "clone", "--depth=1", "git@github.com:" .. repo .. ".git", install_path } - end -end - if os.getenv("CI") then vim.opt.runtimepath:prepend(vim.fn.getcwd()) vim.cmd([[runtime! plugin/plenary.vim]]) vim.cmd([[runtime! plugin/neogit.lua]]) else - ensure_installed("nvim-lua/plenary.nvim") - ensure_installed("nvim-telescope/telescope.nvim") + util.ensure_installed("nvim-lua/plenary.nvim", util.neogit_test_base_dir) end require("plenary.test_harness").test_directory( diff --git a/tests/util/util.lua b/tests/util/util.lua index 30fb7c29..b2e06585 100644 --- a/tests/util/util.lua +++ b/tests/util/util.lua @@ -48,4 +48,24 @@ function M.create_temp_dir(suffix) return prefix .. vim.trim(M.system(cmd)) end +function M.ensure_installed(repo, path) + local name = repo:match(".+/(.+)$") + + local install_path = path .. name + + vim.opt.runtimepath:prepend(install_path) + + if not vim.loop.fs_stat(install_path) then + print("* Downloading " .. name .. " to '" .. install_path .. "/'") + vim.fn.system { "git", "clone", "--depth=1", "git@github.com:" .. repo .. ".git", install_path } + + if vim.v.shell_error > 0 then + error(string.format("! Failed to clone plugin: '%s' in '%s'!", name, install_path), + vim.log.levels.ERROR) + end + end + + print(vim.fn.system("ls " ..install_path)) +end + return M