├── Gemfile ├── stylua.toml ├── tests ├── minitest_examples │ ├── rails_system_test.rb │ ├── system_test_case.rb │ ├── namespaced_rails_system_test.rb │ ├── classic_test.rb │ ├── rails_unit_erroring_test.rb │ ├── reporters_test.rb │ ├── rails_unit_test.rb │ ├── spec_test.rb │ ├── rails_spec_test.rb │ ├── application_system_test_case.rb │ ├── rails_integration_test.rb │ └── rails_module_test.rb ├── adapter │ ├── minimal_init.lua │ ├── treesitter_installed_spec.lua │ ├── plugin_spec.lua │ ├── rails_integration_spec.lua │ ├── rails_system_spec.lua │ ├── rails_module_spec.lua │ ├── utils_spec.lua │ ├── rails_unit_spec.lua │ ├── classic_spec.lua │ └── spec_spec.lua └── outputs │ └── assert_equal_failure.txt ├── .github └── workflows │ └── ci.yaml ├── lua └── neotest-minitest │ ├── config.lua │ ├── utils.lua │ └── init.lua ├── .gitignore ├── Makefile ├── LICENSE.md ├── Gemfile.lock └── README.md /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'minitest' 4 | gem 'minitest-rails' 5 | gem 'minitest-reporters' 6 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 120 2 | indent_type = 'Spaces' 3 | indent_width = 2 4 | no_call_parentheses = false 5 | collapse_simple_statement = "ConditionalOnly" 6 | -------------------------------------------------------------------------------- /tests/minitest_examples/rails_system_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../application_system_test_case" 2 | 3 | class RailsSystemTest < ApplicationSystemTestCase 4 | test "should pass" do 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /tests/minitest_examples/system_test_case.rb: -------------------------------------------------------------------------------- 1 | require_relative "../application_system_test_case" 2 | 3 | class RailsSystemTest < ActionDispatch::SystemTestCase 4 | test "should pass" do 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /tests/minitest_examples/namespaced_rails_system_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../application_system_test_case' 2 | 3 | class Identity::EmailsTest < ApplicationSystemTestCase 4 | test 'should pass' do 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /tests/minitest_examples/classic_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest/autorun' 4 | 5 | class ClassicTest < Minitest::Test 6 | def test_addition 7 | assert_equal 2, 1 + 1 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /tests/minitest_examples/rails_unit_erroring_test.rb: -------------------------------------------------------------------------------- 1 | require 'non_exising_file' 2 | require 'minitest/autorun' 3 | require 'active_support/test_case' 4 | 5 | class RailsUnitErroringTest < ActiveSupport::TestCase 6 | def test_addition 7 | assert_equal 1 + 1, 2 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /tests/minitest_examples/reporters_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest/autorun' 4 | require "minitest/reporters" 5 | 6 | Minitest::Reporters.use! Minitest::Reporters::ProgressReporter.new 7 | 8 | class ReportersTest < Minitest::Test 9 | def test_addition 10 | assert_equal 2, 1 + 1 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /tests/minitest_examples/rails_unit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest/autorun' 4 | require 'active_support/test_case' 5 | 6 | class RailsUnitTest < ActiveSupport::TestCase 7 | test 'adds two numbers' do 8 | assert_equal 2 + 2, 4 9 | end 10 | test 'subtracts two numbers' do 11 | assert_equal 3 - 2, 1 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | jobs: 4 | build: 5 | name: Run tests 6 | runs-on: ubuntu-22.04 7 | 8 | steps: 9 | - uses: actions/checkout@v3 10 | 11 | - uses: rhysd/action-setup-vim@v1 12 | with: 13 | neovim: true 14 | 15 | - name: Run tests 16 | run: | 17 | nvim --version 18 | make test 19 | -------------------------------------------------------------------------------- /lua/neotest-minitest/config.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | M.get_test_cmd = function() 4 | return vim.tbl_flatten({ 5 | "bundle", 6 | "exec", 7 | "ruby", 8 | "-Itest", 9 | }) 10 | end 11 | 12 | M.transform_spec_path = function(path) 13 | return path 14 | end 15 | 16 | M.results_path = function() 17 | return require("neotest.async").fn.tempname() 18 | end 19 | 20 | return M 21 | -------------------------------------------------------------------------------- /tests/minitest_examples/spec_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest/spec' 4 | require 'minitest/autorun' 5 | 6 | describe 'SpecTest' do 7 | describe 'addition' do 8 | it 'adds two numbers' do 9 | assert_equal 2 + 2, 5 10 | end 11 | end 12 | 13 | describe 'subtraction' do 14 | it 'subtracts two numbers' do 15 | assert_equal 3 - 2, 1 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /tests/adapter/minimal_init.lua: -------------------------------------------------------------------------------- 1 | vim.o.swapfile = false 2 | vim.bo.swapfile = false 3 | 4 | vim.cmd([[set runtimepath+=.]]) 5 | vim.cmd([[set runtimepath+=./misc/neotest]]) 6 | vim.cmd([[set runtimepath+=./misc/nio]]) 7 | vim.cmd([[set runtimepath+=./misc/plenary]]) 8 | vim.cmd([[set runtimepath+=./misc/treesitter]]) 9 | 10 | require("nvim-treesitter.configs").setup({ 11 | ensure_installed = "ruby", 12 | sync_install = true, 13 | }) 14 | 15 | require("neotest").setup({ 16 | adapters = { 17 | require("neotest-minitest"), 18 | }, 19 | }) 20 | -------------------------------------------------------------------------------- /tests/minitest_examples/rails_spec_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # RailsSpecTest captures what it looks like to write a Rails TestCase using 4 | # minitest DSL. One way to enable this in a Rails project is by using: 5 | # https://github.com/metaskills/minitest-spec-rails 6 | class RailsSpecTest < ActiveSupport::TestCase 7 | context 'addition' do 8 | test 'adds two numbers' do 9 | assert_equal 2 + 2, 5 10 | end 11 | end 12 | 13 | context 'subtraction' do 14 | test 'subtracts two numbers' do 15 | assert_equal 3 - 2, 1 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | # Others 43 | misc 44 | test_output.txt 45 | output.json 46 | .luarc.json 47 | -------------------------------------------------------------------------------- /tests/minitest_examples/application_system_test_case.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'active_support/message_encryptor' 3 | require 'active_support/dependencies/autoload' 4 | require 'active_support/test_case' 5 | require 'action_controller/template_assertions' 6 | require 'action_dispatch/http/mime_type' 7 | require 'action_dispatch/testing/assertions' 8 | require 'action_dispatch/testing/test_process' 9 | require 'action_dispatch/testing/request_encoder' 10 | require 'action_dispatch/routing' 11 | require 'action_dispatch/system_test_case' 12 | 13 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 14 | end 15 | 16 | -------------------------------------------------------------------------------- /tests/outputs/assert_equal_failure.txt: -------------------------------------------------------------------------------- 1 | Running 3 tests in a single process (parallelization threshold is 50) 2 | Run options: --name /UserInfoControllerTest\#test_throwaway/ -v --seed 7565 3 | 4 | # Running: 5 | 6 | UserInfoControllerTest#test_throwaway = 0.07 s = F 7 | 8 | 9 | Failure: 10 | UserInfoControllerTest#test_throwaway [test/controllers/caregiver_onboarding/user_info_controller_test.rb:21]: 11 | Expected: 2 12 | Actual: 3 13 | 14 | 15 | bin/rails test test/controllers/caregiver_onboarding/user_info_controller_test.rb:20 16 | 17 | 18 | Finished in 0.073849s, 13.5411 runs/s, 13.5411 assertions/s. 19 | 1 runs, 1 assertions, 1 failures, 0 errors, 0 skips 20 | 21 | -------------------------------------------------------------------------------- /tests/minitest_examples/rails_integration_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest/autorun' 4 | require 'active_support/message_encryptor' 5 | require 'active_support/dependencies/autoload' 6 | require 'active_support/test_case' 7 | require 'action_controller/template_assertions' 8 | require 'action_dispatch/http/mime_type' 9 | require 'action_dispatch/testing/assertions' 10 | require 'action_dispatch/testing/test_process' 11 | require 'action_dispatch/testing/request_encoder' 12 | require 'action_dispatch/routing' 13 | require 'action_dispatch/testing/integration' 14 | 15 | class RailsIntegrationTest < ActionDispatch::IntegrationTest 16 | test "should pass" do 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /tests/minitest_examples/rails_module_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "minitest/autorun" 4 | require "active_support/message_encryptor" 5 | require "active_support/dependencies/autoload" 6 | require "active_support/test_case" 7 | require "action_controller/template_assertions" 8 | require "action_dispatch/http/mime_type" 9 | require "action_dispatch/testing/assertions" 10 | require "action_dispatch/testing/test_process" 11 | require "action_dispatch/testing/request_encoder" 12 | require "action_dispatch/routing" 13 | require "action_dispatch/testing/integration" 14 | 15 | module FooController 16 | end 17 | 18 | class FooController::FooControllerTest < ActionDispatch::IntegrationTest 19 | test "should pass" do 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /tests/adapter/treesitter_installed_spec.lua: -------------------------------------------------------------------------------- 1 | local function is_treesitter_installed() 2 | local ok, _ = pcall(require, "nvim-treesitter.configs") 3 | return ok 4 | end 5 | 6 | local function is_ruby_parser_installed() 7 | local ok, parsers = pcall(require, "nvim-treesitter.parsers") 8 | if not ok then return false end 9 | 10 | local has_ruby = parsers.has_parser("ruby") 11 | return has_ruby 12 | end 13 | 14 | describe("Treesitter check", function() 15 | it("should check if Treesitter is installed and working", function() 16 | local treesitter_installed = is_treesitter_installed() 17 | assert.are.same(true, treesitter_installed) 18 | end) 19 | 20 | it("should check if the Ruby parser is installed and working", function() 21 | local ruby_parser_installed = is_ruby_parser_installed() 22 | assert.are.same(true, ruby_parser_installed) 23 | end) 24 | end) 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NEOTEST_DIR = misc/neotest 2 | NIO_DIR = misc/nio 3 | PLENARY_DIR = misc/plenary 4 | TREESITTER_DIR = misc/treesitter 5 | TEST_DIR = tests/adapter 6 | 7 | test: $(NEOTEST_DIR) $(NIO_DIR) $(PLENARY_DIR) $(TREESITTER_DIR) 8 | nvim --headless --clean \ 9 | -u $(TEST_DIR)/minimal_init.lua \ 10 | -c "PlenaryBustedDirectory $(TEST_DIR) { minimal_init = 'tests/adapter/minimal_init.lua' }" 11 | 12 | $(NEOTEST_DIR): 13 | git clone --depth=1 --no-single-branch https://github.com/nvim-neotest/neotest $(NEOTEST_DIR) 14 | @rm -rf $(NEOTEST_DIR)/.git 15 | 16 | $(NIO_DIR): 17 | git clone --depth=1 --no-single-branch https://github.com/nvim-neotest/nvim-nio $(NIO_DIR) 18 | @rm -rf $(NIO_DIR)/.git 19 | 20 | 21 | $(PLENARY_DIR): 22 | git clone --depth=1 --no-single-branch https://github.com/nvim-lua/plenary.nvim $(PLENARY_DIR) 23 | @rm -rf $(PLENARY_DIR)/.git 24 | 25 | $(TREESITTER_DIR): 26 | git clone --depth=1 --no-single-branch https://github.com/nvim-treesitter/nvim-treesitter $(TREESITTER_DIR) 27 | @rm -rf $(TREESITTER_DIR)/.git 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Hussein Al Abry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /tests/adapter/plugin_spec.lua: -------------------------------------------------------------------------------- 1 | local plugin = require("neotest-minitest") 2 | 3 | describe("is_test_file", function() 4 | it("matches Rails-style test file", function() 5 | assert.equals(true, plugin.is_test_file("./test/foo_test.rb")) 6 | end) 7 | 8 | it("matches minitest-style test file", function() 9 | assert.equals(true, plugin.is_test_file("./test/test_foo.rb")) 10 | end) 11 | 12 | it("does not match plain ruby files", function() 13 | assert.equals(false, plugin.is_test_file("./lib/foo.rb")) 14 | end) 15 | end) 16 | 17 | describe("filter_dir", function() 18 | local root = "/home/name/projects" 19 | it("allows test", function() 20 | assert.equals(true, plugin.filter_dir("test", "test", root)) 21 | end) 22 | it("allows sub directories one deep (for engines)", function() 23 | assert.equals(true, plugin.filter_dir("test_engine", "test_engine", root)) 24 | end) 25 | it("allows paths that contain test", function() 26 | assert.equals(true, plugin.filter_dir("test", "test_engine/test", root)) 27 | end) 28 | it("allows a long path with test at the start", function() 29 | assert.equals(true, plugin.filter_dir("billing_service", "test/controllers/billing_service", root)) 30 | end) 31 | it("allows paths without test, more that one sub dir deep", function() 32 | assert.equals(true, plugin.filter_dir("models", "app/models", root)) 33 | end) 34 | it("disallows the vendor directory", function() 35 | assert.equals(false, plugin.filter_dir("vendor", "vendor", root)) 36 | end) 37 | end) 38 | -------------------------------------------------------------------------------- /tests/adapter/rails_integration_spec.lua: -------------------------------------------------------------------------------- 1 | local plugin = require("neotest-minitest") 2 | local async = require("nio.tests") 3 | 4 | describe("Rails Integration Test", function() 5 | describe("discover_positions", function() 6 | async.it("should discover the position for the tests", function() 7 | local test_path = vim.loop.cwd() .. "/tests/minitest_examples/rails_integration_test.rb" 8 | local positions = plugin.discover_positions(test_path):to_list() 9 | local expected_positions = { 10 | { 11 | id = test_path, 12 | name = "rails_integration_test.rb", 13 | path = test_path, 14 | range = { 0, 0, 18, 0 }, 15 | type = "file", 16 | }, 17 | { 18 | { 19 | id = "./tests/minitest_examples/rails_integration_test.rb::15", 20 | name = "RailsIntegrationTest", 21 | path = test_path, 22 | range = { 14, 0, 17, 3 }, 23 | type = "namespace", 24 | }, 25 | { 26 | { 27 | id = "./tests/minitest_examples/rails_integration_test.rb::16", 28 | name = "should pass", 29 | path = test_path, 30 | range = { 15, 2, 16, 5 }, 31 | type = "test", 32 | }, 33 | }, 34 | }, 35 | } 36 | assert.are.same(positions, expected_positions) 37 | end) 38 | end) 39 | describe("_parse_test_output", function() 40 | describe("single passing test", function() 41 | local output = [[ 42 | UserControllerTest#test_is_site-admin = 0.25 s = . 43 | ]] 44 | it("parses the results correctly", function() 45 | local results = plugin._parse_test_output(output, { ["UserControllerTest#test_is_site-admin"] = "testing" }) 46 | 47 | assert.are.same({ ["testing"] = { status = "passed" } }, results) 48 | end) 49 | end) 50 | end) 51 | end) 52 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionpack (7.0.4.3) 5 | actionview (= 7.0.4.3) 6 | activesupport (= 7.0.4.3) 7 | rack (~> 2.0, >= 2.2.0) 8 | rack-test (>= 0.6.3) 9 | rails-dom-testing (~> 2.0) 10 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 11 | actionview (7.0.4.3) 12 | activesupport (= 7.0.4.3) 13 | builder (~> 3.1) 14 | erubi (~> 1.4) 15 | rails-dom-testing (~> 2.0) 16 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 17 | activesupport (7.0.4.3) 18 | concurrent-ruby (~> 1.0, >= 1.0.2) 19 | i18n (>= 1.6, < 2) 20 | minitest (>= 5.1) 21 | tzinfo (~> 2.0) 22 | ansi (1.5.0) 23 | builder (3.2.4) 24 | concurrent-ruby (1.2.2) 25 | crass (1.0.6) 26 | erubi (1.12.0) 27 | i18n (1.12.0) 28 | concurrent-ruby (~> 1.0) 29 | loofah (2.20.0) 30 | crass (~> 1.0.2) 31 | nokogiri (>= 1.5.9) 32 | method_source (1.0.0) 33 | mini_portile2 (2.8.6) 34 | minitest (5.18.0) 35 | minitest-rails (7.0.0) 36 | minitest (~> 5.10) 37 | railties (~> 7.0.0) 38 | minitest-reporters (1.6.0) 39 | ansi 40 | builder 41 | minitest (>= 5.0) 42 | ruby-progressbar 43 | nokogiri (1.14.3) 44 | mini_portile2 (~> 2.8.0) 45 | racc (~> 1.4) 46 | racc (1.6.2) 47 | rack (2.2.6.4) 48 | rack-test (2.1.0) 49 | rack (>= 1.3) 50 | rails-dom-testing (2.0.3) 51 | activesupport (>= 4.2.0) 52 | nokogiri (>= 1.6) 53 | rails-html-sanitizer (1.5.0) 54 | loofah (~> 2.19, >= 2.19.1) 55 | railties (7.0.4.3) 56 | actionpack (= 7.0.4.3) 57 | activesupport (= 7.0.4.3) 58 | method_source 59 | rake (>= 12.2) 60 | thor (~> 1.0) 61 | zeitwerk (~> 2.5) 62 | rake (13.0.6) 63 | ruby-progressbar (1.13.0) 64 | thor (1.2.1) 65 | tzinfo (2.0.6) 66 | concurrent-ruby (~> 1.0) 67 | zeitwerk (2.6.7) 68 | 69 | PLATFORMS 70 | arm64-darwin-21 71 | arm64-darwin-23 72 | 73 | DEPENDENCIES 74 | minitest 75 | minitest-rails 76 | minitest-reporters 77 | 78 | BUNDLED WITH 79 | 2.3.26 80 | -------------------------------------------------------------------------------- /tests/adapter/rails_system_spec.lua: -------------------------------------------------------------------------------- 1 | local plugin = require("neotest-minitest") 2 | local async = require("nio.tests") 3 | 4 | describe("Rails System Test", function() 5 | assert:set_parameter("TableFormatLevel", -1) 6 | describe("discover_positions SystemTestCase", function() 7 | async.it("should discover the position for the tests", function() 8 | local test_path = vim.loop.cwd() .. "/tests/minitest_examples/system_test_case.rb" 9 | local positions = plugin.discover_positions(test_path):to_list() 10 | local expected_positions = { 11 | { 12 | id = test_path, 13 | name = "system_test_case.rb", 14 | path = test_path, 15 | range = { 0, 0, 6, 0 }, 16 | type = "file", 17 | }, 18 | { 19 | { 20 | id = "./tests/minitest_examples/system_test_case.rb::3", 21 | name = "RailsSystemTest", 22 | path = test_path, 23 | range = { 2, 0, 5, 3 }, 24 | type = "namespace", 25 | }, 26 | { 27 | { 28 | id = "./tests/minitest_examples/system_test_case.rb::4", 29 | name = "should pass", 30 | path = test_path, 31 | range = { 3, 2, 4, 5 }, 32 | type = "test", 33 | }, 34 | }, 35 | }, 36 | } 37 | assert.are.same(positions, expected_positions) 38 | end) 39 | end) 40 | 41 | describe("discover_positions ApplicationSystemTestCase", function() 42 | async.it("should discover the position for the tests", function() 43 | local test_path = vim.loop.cwd() .. "/tests/minitest_examples/rails_system_test.rb" 44 | local positions = plugin.discover_positions(test_path):to_list() 45 | local expected_positions = { 46 | { 47 | id = test_path, 48 | name = "rails_system_test.rb", 49 | path = test_path, 50 | range = { 0, 0, 6, 0 }, 51 | type = "file", 52 | }, 53 | { 54 | { 55 | id = "./tests/minitest_examples/rails_system_test.rb::3", 56 | name = "RailsSystemTest", 57 | path = test_path, 58 | range = { 2, 0, 5, 3 }, 59 | type = "namespace", 60 | }, 61 | { 62 | { 63 | id = "./tests/minitest_examples/rails_system_test.rb::4", 64 | name = "should pass", 65 | path = test_path, 66 | range = { 3, 2, 4, 5 }, 67 | type = "test", 68 | }, 69 | }, 70 | }, 71 | } 72 | assert.are.same(positions, expected_positions) 73 | end) 74 | end) 75 | 76 | describe("discover_positions namespaced ApplicationSystemTestCase", function() 77 | async.it("should discover the position for the tests", function() 78 | local test_path = vim.loop.cwd() .. "/tests/minitest_examples/namespaced_rails_system_test.rb" 79 | local positions = plugin.discover_positions(test_path):to_list() 80 | local expected_positions = { 81 | { 82 | id = test_path, 83 | name = "namespaced_rails_system_test.rb", 84 | path = test_path, 85 | range = { 0, 0, 6, 0 }, 86 | type = "file", 87 | }, 88 | { 89 | { 90 | id = "./tests/minitest_examples/namespaced_rails_system_test.rb::3", 91 | name = "EmailsTest", 92 | path = test_path, 93 | range = { 2, 0, 5, 3 }, 94 | type = "namespace", 95 | }, 96 | { 97 | { 98 | id = "./tests/minitest_examples/namespaced_rails_system_test.rb::4", 99 | name = "should pass", 100 | path = test_path, 101 | range = { 3, 2, 4, 5 }, 102 | type = "test", 103 | }, 104 | }, 105 | }, 106 | } 107 | assert.are.same(positions, expected_positions) 108 | end) 109 | end) 110 | end) 111 | -------------------------------------------------------------------------------- /tests/adapter/rails_module_spec.lua: -------------------------------------------------------------------------------- 1 | local plugin = require("neotest-minitest") 2 | local async = require("nio.tests") 3 | 4 | describe("Rails Module Test", function() 5 | assert:set_parameter("TableFormatLevel", -1) 6 | describe("discover_positions", function() 7 | async.it("should discover the position for the tests", function() 8 | local test_path = vim.loop.cwd() .. "/tests/minitest_examples/rails_module_test.rb" 9 | local positions = plugin.discover_positions(test_path):to_list() 10 | local expected_positions = { 11 | { 12 | id = test_path, 13 | name = "rails_module_test.rb", 14 | path = test_path, 15 | range = { 0, 0, 21, 0 }, 16 | type = "file", 17 | }, 18 | { 19 | { 20 | id = "./tests/minitest_examples/rails_module_test.rb::18", 21 | name = "FooControllerTest", 22 | path = test_path, 23 | range = { 17, 0, 20, 3 }, 24 | type = "namespace", 25 | }, 26 | { 27 | { 28 | id = "./tests/minitest_examples/rails_module_test.rb::19", 29 | name = "should pass", 30 | path = test_path, 31 | range = { 18, 2, 19, 5 }, 32 | type = "test", 33 | }, 34 | }, 35 | }, 36 | } 37 | assert.are.same(expected_positions, positions) 38 | end) 39 | end) 40 | describe("_parse_test_output", function() 41 | describe("single error test", function() 42 | local output = [[ 43 | CaregiverOnboarding::UserInfoControllerTest#test_should_get_edit = 0.00 s = E 44 | 45 | 46 | Error: 47 | CaregiverOnboarding::UserInfoControllerTest#test_should_get_edit: 48 | NameError: undefined local variable or method `foobar' for an instance of CaregiverOnboarding::UserInfoControllerTest 49 | test/controllers/caregiver_onboarding/user_info_controller_test.rb:5:in `block in ' 50 | ]] 51 | it("parses the results correctly", function() 52 | local results = 53 | plugin._parse_test_output(output, { ["UserInfoControllerTest#test_should_get_edit"] = "testing" }) 54 | assert.are.same({ 55 | ["testing"] = { 56 | status = "failed", 57 | errors = { 58 | { 59 | line = 4, 60 | message = "NameError: undefined local variable or method `foobar' for an instance of CaregiverOnboarding::UserInfoControllerTest", 61 | }, 62 | }, 63 | }, 64 | }, results) 65 | end) 66 | end) 67 | 68 | describe("multiple passing tests", function() 69 | local output = [[ 70 | Foo::RailsUnitTest#test_subtracts_two_numbers = 0.00 s = . 71 | Foo::RailsUnitTest#test_adds_two_numbers = 0.00 s = . 72 | ]] 73 | 74 | it("parses the results correctly", function() 75 | -- We end up only parsing the class name, not the module 76 | local results = plugin._parse_test_output(output, { 77 | ["RailsUnitTest#test_adds_two_numbers"] = "testing", 78 | ["RailsUnitTest#test_subtracts_two_numbers"] = "testing2", 79 | }) 80 | 81 | assert.are.same({ 82 | ["testing"] = { status = "passed" }, 83 | ["testing2"] = { status = "passed" }, 84 | }, results) 85 | end) 86 | end) 87 | 88 | describe("single failing test", function() 89 | local output = [[ 90 | CaregiverOnboarding::UserInfoControllerTest#test_throwaway = 0.07 s = F 91 | 92 | 93 | Failure: 94 | CaregiverOnboarding::UserInfoControllerTest#test_throwaway [test/controllers/caregiver_onboarding/user_info_controller_test.rb:21]: 95 | Expected: 2 96 | Actual: 4 97 | 98 | 99 | ]] 100 | 101 | it("parses the results correctly", function() 102 | local results = plugin._parse_test_output(output, { ["UserInfoControllerTest#test_throwaway"] = "testing" }) 103 | 104 | assert.are.same({ 105 | ["testing"] = { status = "failed", errors = { { message = "Expected: 2\n Actual: 4", line = 20 } } }, 106 | }, results) 107 | end) 108 | end) 109 | end) 110 | end) 111 | -------------------------------------------------------------------------------- /lua/neotest-minitest/utils.lua: -------------------------------------------------------------------------------- 1 | local ok, async = pcall(require, "nio") 2 | if not ok then async = require("neotest.async") end 3 | 4 | local logger = require("neotest.logging") 5 | 6 | local M = {} 7 | local separator = "::" 8 | 9 | --- Replace paths in a string 10 | ---@param str string 11 | ---@param what string 12 | ---@param with string 13 | ---@return string 14 | local function replace_paths(str, what, with) 15 | -- Taken from: https://stackoverflow.com/a/29379912/3250992 16 | what = string.gsub(what, "[%(%)%.%+%-%*%?%[%]%^%$%%]", "%%%1") -- escape pattern 17 | with = string.gsub(with, "[%%]", "%%%%") -- escape replacement 18 | return string.gsub(str, what, with) 19 | end 20 | 21 | -- We are considering test class names without their module, but 22 | -- Lua's built-in pattern matching isn't powerful enough to do so. Instead 23 | -- we match on the full name, including module, and strip it off here. 24 | -- 25 | -- @param test_name string 26 | -- @return string 27 | M.replace_module_namespace = function(test_name) 28 | return test_name.gsub(test_name, "%w+::", "") 29 | end 30 | 31 | ---@param position neotest.Position The position to return an ID for 32 | ---@param parents neotest.Position[] Parent positions for the position 33 | ---@return string 34 | M.generate_treesitter_id = function(position, parents) 35 | local cwd = async.fn.getcwd() 36 | local test_path = "." .. replace_paths(position.path, cwd, "") 37 | -- Treesitter starts line numbers from 0 so we subtract 1 38 | local id = test_path .. separator .. (tonumber(position.range[1]) + 1) 39 | 40 | return id 41 | end 42 | 43 | M.full_spec_name = function(tree) 44 | local name = "" 45 | local namespaces = {} 46 | local num_namespaces = 0 47 | 48 | if tree:data().type == "namespace" then 49 | table.insert(namespaces, 1, tree:data().name) 50 | num_namespaces = num_namespaces + 1 51 | else 52 | name = tree:data().name 53 | end 54 | 55 | for parent_node in tree:iter_parents() do 56 | local data = parent_node:data() 57 | if data.type == "namespace" then 58 | table.insert(namespaces, 1, parent_node:data().name) 59 | num_namespaces = num_namespaces + 1 60 | else 61 | break 62 | end 63 | end 64 | 65 | if num_namespaces == 0 then return name end 66 | 67 | -- build result 68 | local result = "" 69 | 70 | -- assemble namespaces 71 | result = table.concat(namespaces, "::") 72 | 73 | if name == "" then return result end 74 | 75 | -- add # separator 76 | result = result .. "#" 77 | -- add test_ prefix 78 | result = result .. "test_" 79 | -- add index 80 | for i, child_tree in ipairs(tree:parent():children()) do 81 | for _, node in child_tree:iter_nodes() do 82 | if node:data().id == tree:data().id then result = result .. string.format("%04d", i) end 83 | end 84 | end 85 | -- add _[name] 86 | result = result .. "_" .. name 87 | 88 | return result 89 | end 90 | 91 | M.full_test_name = function(tree) 92 | local name = tree:data().name 93 | local parent_tree = tree:parent() 94 | if not parent_tree or parent_tree:data().type == "file" then return name end 95 | local parent_name = parent_tree:data().name 96 | 97 | -- For rails and spec tests 98 | if not name:match("^test_") then name = "test_" .. name end 99 | 100 | return parent_name .. "#" .. name:gsub(" ", "_") 101 | end 102 | 103 | M.escaped_full_test_name = function(tree) 104 | local full_name = M.full_test_name(tree) 105 | return full_name:gsub("([?#])", "\\%1") 106 | end 107 | 108 | M.get_mappings = function(tree) 109 | -- get the mappings for the current node and its children 110 | local mappings = {} 111 | local function name_map(tree) 112 | local data = tree:data() 113 | if data.type == "test" then 114 | local full_spec_name = M.full_spec_name(tree) 115 | mappings[full_spec_name] = data.id 116 | 117 | local full_test_name = M.full_test_name(tree) 118 | mappings[full_test_name] = data.id 119 | end 120 | 121 | for _, child in ipairs(tree:children()) do 122 | name_map(child) 123 | end 124 | end 125 | name_map(tree) 126 | 127 | return mappings 128 | end 129 | 130 | M.strip_ansi_escape_codes = function(str) 131 | return str:gsub("\27%[%d+m", "") 132 | end 133 | 134 | return M 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # neotest-minitest 2 | 3 | This plugin provides a [minitest](https://docs.seattlerb.org/minitest/) adapter for the [Neotest](https://github.com/nvim-neotest/neotest) framework. 4 | 5 | ## :package: Installation 6 | 7 | Install with the package manager of your choice: 8 | 9 | **Lazy** 10 | 11 | ```lua 12 | { 13 | "nvim-neotest/neotest", 14 | lazy = true, 15 | dependencies = { 16 | ..., 17 | "zidhuss/neotest-minitest", 18 | }, 19 | config = function() 20 | require("neotest").setup({ 21 | ..., 22 | adapters = { 23 | require("neotest-minitest") 24 | }, 25 | }) 26 | end 27 | } 28 | ``` 29 | 30 |
31 | Packer 32 | 33 | ```lua 34 | use({ 35 | 'nvim-neotest/neotest', 36 | requires = { 37 | ..., 38 | 'zidhuss/neotest-minitest', 39 | }, 40 | config = function() 41 | require('neotest').setup({ 42 | ..., 43 | adapters = { 44 | require('neotest-minitest'), 45 | } 46 | }) 47 | end 48 | }) 49 | ``` 50 |
51 | 52 | ## :wrench: Configuration 53 | 54 | ### Default configuration 55 | 56 | > **Note**: You only need to the call the `setup` function if you wish to change any of the defaults 57 | 58 |
59 | Show default configuration 60 | 61 | ```lua 62 | adapters = { 63 | require("neotest-minitest")({ 64 | test_cmd = function() 65 | return vim.tbl_flatten({ 66 | "bundle", 67 | "exec", 68 | "ruby", 69 | "-Itest", 70 | }) 71 | end, 72 | }), 73 | } 74 | ``` 75 | 76 |
77 | 78 | ### The test command 79 | 80 | The command used to run tests can be changed via the `test_cmd` option e.g. 81 | 82 | ```lua 83 | require("neotest-minitest")({ 84 | test_cmd = function() 85 | return vim.tbl_flatten({ 86 | "bundle", 87 | "exec", 88 | "rails", 89 | "test", 90 | }) 91 | end 92 | }) 93 | ``` 94 | 95 | ### Running tests in a Docker container 96 | 97 | The following configuration overrides `test_cmd` to run a Docker container (using `docker-compose`) and overrides `transform_spec_path` to pass the spec file as a relative path instead of an absolute path to Minitest. The `results_path` needs to be set to a location which is available to both the container and the host. 98 | 99 | ```lua 100 | require("neotest").setup({ 101 | adapters = { 102 | require("neotest-minitest")({ 103 | test_cmd = function() 104 | return vim.tbl_flatten({ 105 | "docker", 106 | "compose", 107 | "exec", 108 | "-i", 109 | "-w", "/app", 110 | "-e", "RAILS_ENV=test", 111 | "app", 112 | "bundle", 113 | "exec", 114 | "test" 115 | }) 116 | end, 117 | 118 | transform_spec_path = function(path) 119 | local prefix = require('neotest-minitest').root(path) 120 | return string.sub(path, string.len(prefix) + 2, -1) 121 | end, 122 | 123 | results_path = "tmp/minitest.output" 124 | }) 125 | } 126 | }) 127 | ``` 128 | 129 | Alternatively, you can accomplish this using a shell script as your Minitest command. See [this comment](https://github.com/nvim-neotest/neotest/issues/89#issuecomment-1338141432) for an example. 130 | 131 | ## :rocket: Usage 132 | 133 | _NOTE_: All usages of `require('neotest').run.run` can be mapped to a command in your config (this is not included and should be done by yourself). 134 | 135 | #### Test single function 136 | 137 | To test a single test, hover over the test and run `require('neotest').run.run()` 138 | 139 | #### Test file 140 | 141 | To test a file run `require('neotest').run.run(vim.fn.expand('%'))` 142 | 143 | ## :gift: Contributing 144 | 145 | This project is maintained by the Neovim Ruby community. Please raise a PR if you are interested in adding new functionality or fixing any bugs. When submitting a bug, please include an example test. 146 | 147 | To trigger the tests for the adapter, run: 148 | 149 | ```sh 150 | make test 151 | ``` 152 | 153 | ## :clap: Thanks 154 | 155 | Special thanks to [Oli Morris](https://github.com/olimorris) and others for their work on [neotest-rspec](https://github.com/olimorris/neotest-rspec) that inspired this adapter. 156 | -------------------------------------------------------------------------------- /tests/adapter/utils_spec.lua: -------------------------------------------------------------------------------- 1 | local utils = require("neotest-minitest.utils") 2 | local Tree = require("neotest.types.tree") 3 | 4 | describe("generate_treesitter_id", function() 5 | it("forms an id", function() 6 | local ts = { 7 | name = "'adds two numbers together'", 8 | path = vim.loop.cwd() .. "/tests/classic/classic_test.rb", 9 | range = { 10 | 1, 11 | 2, 12 | 3, 13 | 5, 14 | }, 15 | type = "test", 16 | } 17 | 18 | assert.equals("./tests/classic/classic_test.rb::2", utils.generate_treesitter_id(ts)) 19 | end) 20 | end) 21 | 22 | describe("full_spec_name", function() 23 | it("concatenates namespaces with :: separator", function() 24 | local tree = Tree.from_list({ 25 | { id = "namespace1", name = "namespace1", type = "namespace" }, 26 | { 27 | { id = "namespace2", name = "namespace2", type = "namespace" }, 28 | { 29 | { id = "namespace3", name = "namespace3", type = "namespace" }, 30 | { 31 | { id = "test", name = "example" }, 32 | }, 33 | }, 34 | }, 35 | }, function(pos) 36 | return pos.id 37 | end) 38 | 39 | assert.equals("namespace1::namespace2::namespace3", utils.full_spec_name(tree:children()[1]:children()[1])) 40 | assert.equals( 41 | "namespace1::namespace2::namespace3#test_0001_example", 42 | utils.full_spec_name(tree:children()[1]:children()[1]:children()[1]) 43 | ) 44 | end) 45 | 46 | it("includes a zero-padded test index", function() 47 | local tree = Tree.from_list({ 48 | { id = "namespace1", name = "namespace1", type = "namespace" }, 49 | { 50 | { id = "namespace2", name = "namespace2", type = "namespace" }, 51 | { 52 | { id = "namespace3", name = "namespace3", type = "namespace" }, 53 | { 54 | { id = "test1", name = "example1" }, 55 | }, 56 | { 57 | { id = "test2", name = "example2" }, 58 | }, 59 | { 60 | { id = "test3", name = "example3" }, 61 | }, 62 | }, 63 | }, 64 | }, function(pos) 65 | return pos.id 66 | end) 67 | assert.equals( 68 | "namespace1::namespace2::namespace3#test_0002_example2", 69 | utils.full_spec_name(tree:children()[1]:children()[1]:children()[2]) 70 | ) 71 | end) 72 | 73 | it("does not replace spaces with underscores", function() 74 | local tree = Tree.from_list({ 75 | { id = "namespace1", name = "namespace1", type = "namespace" }, 76 | { 77 | { id = "namespace2", name = "namespace2", type = "namespace" }, 78 | { 79 | { id = "namespace3", name = "namespace3", type = "namespace" }, 80 | { 81 | { id = "test", name = "this is a great test name" }, 82 | }, 83 | }, 84 | }, 85 | }, function(pos) 86 | return pos.id 87 | end) 88 | assert.equals( 89 | "namespace1::namespace2::namespace3#test_0001_this is a great test name", 90 | utils.full_spec_name(tree:children()[1]:children()[1]:children()[1]) 91 | ) 92 | end) 93 | end) 94 | 95 | describe("full_test_name", function() 96 | it("returns the name of the test", function() 97 | local tree = Tree.from_list({ id = "test", name = "test_example" }, function(pos) 98 | return pos.id 99 | end) 100 | assert.equals("test_example", utils.full_test_name(tree)) 101 | end) 102 | 103 | it("returns the name of the test with the parent namespace", function() 104 | local tree = Tree.from_list({ 105 | { id = "namespace", name = "namespace", type = "namespace" }, 106 | { id = "test", name = "example" }, 107 | }, function(pos) 108 | return pos.id 109 | end) 110 | assert.equals("namespace#test_example", utils.full_test_name(tree:children()[1])) 111 | end) 112 | 113 | it("prefixes the test with test_", function() 114 | local tree = Tree.from_list({ 115 | { id = "namespace", name = "namespace", type = "namespace" }, 116 | { id = "test", name = "example" }, 117 | }, function(pos) 118 | return pos.id 119 | end) 120 | assert.equals("namespace#test_example", utils.full_test_name(tree:children()[1])) 121 | end) 122 | 123 | it("replaces spaces with underscores", function() 124 | local tree = Tree.from_list({ 125 | { id = "namespace", name = "namespace", type = "namespace" }, 126 | { id = "test", name = "this is a great test name" }, 127 | }, function(pos) 128 | return pos.id 129 | end) 130 | assert.equals("namespace#test_this_is_a_great_test_name", utils.full_test_name(tree:children()[1])) 131 | end) 132 | 133 | it("shouldn't replace the quotes inside the test name", function() 134 | local tree = Tree.from_list({ 135 | { id = "namespace", name = "namespace", type = "namespace" }, 136 | { id = "test", name = "shouldn't remove our single quote" }, 137 | }, function(pos) 138 | return pos.id 139 | end) 140 | assert.equals("namespace#test_shouldn't_remove_our_single_quote", utils.full_test_name(tree:children()[1])) 141 | end) 142 | end) 143 | 144 | describe("escaped_full_test_name", function() 145 | it("escapes # characters", function() 146 | local tree = Tree.from_list({ 147 | { id = "namespace", name = "namespace", type = "namespace" }, 148 | { id = "test", name = "#escaped_full_test_name should be escaped" }, 149 | }, function(pos) 150 | return pos.id 151 | end) 152 | assert.equals( 153 | "namespace\\#test_\\#escaped_full_test_name_should_be_escaped", 154 | utils.escaped_full_test_name(tree:children()[1]) 155 | ) 156 | end) 157 | 158 | it("escapes ? characters", function() 159 | local tree = Tree.from_list({ 160 | { id = "namespace", name = "namespace", type = "namespace" }, 161 | { id = "test", name = "escaped? should be escaped" }, 162 | }, function(pos) 163 | return pos.id 164 | end) 165 | assert.equals("namespace\\#test_escaped\\?_should_be_escaped", utils.escaped_full_test_name(tree:children()[1])) 166 | end) 167 | 168 | it("escapes multiple ? and # characters", function() 169 | local tree = Tree.from_list({ 170 | { id = "namespace", name = "namespace", type = "namespace" }, 171 | { id = "test", name = "#escaped? should be escaped" }, 172 | }, function(pos) 173 | return pos.id 174 | end) 175 | assert.equals("namespace\\#test_\\#escaped\\?_should_be_escaped", utils.escaped_full_test_name(tree:children()[1])) 176 | end) 177 | end) 178 | 179 | describe("get_mappings", function() 180 | it("gives full test name for nodes of tree", function() 181 | local tree = Tree.from_list({ 182 | { id = "namespace", name = "namespace", type = "namespace" }, 183 | { id = "namespace_test_example", name = "test_example", type = "test" }, 184 | }, function(pos) 185 | return pos.id 186 | end) 187 | 188 | local mappings = utils.get_mappings(tree) 189 | 190 | assert.equals("namespace_test_example", mappings["namespace#test_example"]) 191 | end) 192 | 193 | it("give test name with no nesting", function() 194 | local tree = Tree.from_list({ 195 | { id = "test_id", name = "test", type = "test" }, 196 | }, function(pos) 197 | return pos.id 198 | end) 199 | 200 | local mappings = utils.get_mappings(tree) 201 | 202 | assert.equals("test_id", mappings["test"]) 203 | end) 204 | end) 205 | 206 | describe("strip_ansi", function() 207 | it("strips ansi codes", function() 208 | local input = "This is \27[32mgreen\27[0m text!" 209 | 210 | assert.equals("This is green text!", utils.strip_ansi_escape_codes(input)) 211 | end) 212 | end) 213 | 214 | describe("replace_module_namespace", function() 215 | it("removes module namespace", function() 216 | local input = "Foo::Bar" 217 | 218 | assert.equals("Bar", utils.replace_module_namespace(input)) 219 | end) 220 | end) 221 | -------------------------------------------------------------------------------- /tests/adapter/rails_unit_spec.lua: -------------------------------------------------------------------------------- 1 | local plugin = require("neotest-minitest") 2 | local async = require("nio.tests") 3 | 4 | describe("Rails Unit Test", function() 5 | assert:set_parameter("TableFormatLevel", -1) 6 | describe("discover_positions", function() 7 | async.it("should discover the position for the tests", function() 8 | local test_path = vim.loop.cwd() .. "/tests/minitest_examples/rails_unit_test.rb" 9 | local positions = plugin.discover_positions(test_path):to_list() 10 | local expected_positions = { 11 | { 12 | id = test_path, 13 | name = "rails_unit_test.rb", 14 | path = test_path, 15 | range = { 0, 0, 13, 0 }, 16 | type = "file", 17 | }, 18 | { 19 | { 20 | id = "./tests/minitest_examples/rails_unit_test.rb::6", 21 | name = "RailsUnitTest", 22 | path = test_path, 23 | range = { 5, 0, 12, 3 }, 24 | type = "namespace", 25 | }, 26 | { 27 | { 28 | id = "./tests/minitest_examples/rails_unit_test.rb::7", 29 | name = "adds two numbers", 30 | path = test_path, 31 | range = { 6, 2, 8, 5 }, 32 | type = "test", 33 | }, 34 | }, 35 | { 36 | { 37 | id = "./tests/minitest_examples/rails_unit_test.rb::10", 38 | name = "subtracts two numbers", 39 | path = test_path, 40 | range = { 9, 2, 11, 5 }, 41 | type = "test", 42 | }, 43 | }, 44 | }, 45 | } 46 | 47 | assert.are.same(positions, expected_positions) 48 | end) 49 | end) 50 | 51 | describe("_parse_test_output", function() 52 | describe("single failing test", function() 53 | local output = [[ 54 | RailsUnitTest#test_adds_two_numbers = 0.00 s = F 55 | 56 | 57 | Failure: 58 | RailsUnitTest#test_adds_two_numbers [/src/nvim-neotest/neotest-minitest/tests/minitest_examples/rails_unit_test.rb:8]: 59 | Expected: 4 60 | Actual: 5 61 | 62 | 63 | ]] 64 | it("parses the results correctly", function() 65 | local results = plugin._parse_test_output(output, { ["RailsUnitTest#test_adds_two_numbers"] = "testing" }) 66 | 67 | assert.are.same( 68 | { ["testing"] = { status = "failed", errors = { { message = "Expected: 4\n Actual: 5", line = 7 } } } }, 69 | results 70 | ) 71 | end) 72 | end) 73 | 74 | describe("single passing test with ruby error", function() 75 | local output = [[ 76 | Traceback (most recent call last): 77 | 1: from tests/minitest_examples/rails_unit_erroring_test.rb:1:in `
' 78 | tests/minitest_examples/rails_unit_erroring_test.rb:1:in `require': cannot load such file -- non_exising_file (LoadError) 79 | ]] 80 | 81 | it("parses the results correctly", function() 82 | local results = plugin._parse_test_output(output, { ["RailsUnitErroringTest#test_addition"] = "testing" }) 83 | 84 | assert.are.same({ 85 | ["testing"] = { 86 | status = "failed", 87 | errors = { 88 | { message = "in `require': cannot load such file -- non_exising_file (LoadError)", line = 0 }, 89 | }, 90 | }, 91 | }, results) 92 | end) 93 | end) 94 | 95 | describe("multiple tests with ruby error", function() 96 | local output = [[ 97 | Traceback (most recent call last): 98 | 1: from tests/minitest_examples/rails_unit_erroring_test.rb:1:in `
' 99 | tests/minitest_examples/rails_unit_erroring_test.rb:1:in `require': cannot load such file -- non_exising_file (LoadError) 100 | ]] 101 | 102 | it("parses the results correctly", function() 103 | local results = plugin._parse_test_output(output, { 104 | ["RailsUnitErroringTest#test_addition"] = "testing", 105 | ["RailsUnitTest#test_subtracts_two_numbers"] = "testing1", 106 | }) 107 | 108 | assert.are.same({ 109 | ["testing"] = { 110 | status = "failed", 111 | errors = { 112 | { message = "in `require': cannot load such file -- non_exising_file (LoadError)", line = 0 }, 113 | }, 114 | }, 115 | ["testing1"] = { 116 | status = "failed", 117 | errors = { 118 | { message = "in `require': cannot load such file -- non_exising_file (LoadError)", line = 0 }, 119 | }, 120 | }, 121 | }, results) 122 | end) 123 | end) 124 | 125 | describe("single passing test", function() 126 | local output = [[ 127 | RailsUnitTest#test_subtracts_two_numbers = 0.00 s = . 128 | ]] 129 | 130 | it("parses the results correctly", function() 131 | local results = plugin._parse_test_output(output, { ["RailsUnitTest#test_subtracts_two_numbers"] = "testing" }) 132 | 133 | assert.are.same({ ["testing"] = { status = "passed" } }, results) 134 | end) 135 | end) 136 | 137 | describe("failing and passing tests", function() 138 | local output = [[ 139 | RailsUnitTest#test_subtracts_two_numbers = 0.00 s = . 140 | RailsUnitTest#test_adds_two_numbers = 0.00 s = F 141 | 142 | 143 | Failure: 144 | RailsUnitTest#test_adds_two_numbers [/neotest-minitest/tests/minitest_examples/rails_unit_test.rb:8]: 145 | Expected: 4 146 | Actual: 5 147 | 148 | 149 | ]] 150 | 151 | it("parses the results correctly", function() 152 | local results = plugin._parse_test_output(output, { 153 | ["RailsUnitTest#test_adds_two_numbers"] = "testing", 154 | ["RailsUnitTest#test_subtracts_two_numbers"] = "testing2", 155 | }) 156 | 157 | assert.are.same({ 158 | ["testing"] = { status = "failed", errors = { { message = "Expected: 4\n Actual: 5", line = 7 } } }, 159 | ["testing2"] = { status = "passed" }, 160 | }, results) 161 | end) 162 | end) 163 | 164 | describe("multiple failing tests", function() 165 | local output = [[ 166 | RailsUnitTest#test_adds_two_numbers = 0.00 s = F 167 | 168 | 169 | Failure: 170 | RailsUnitTest#test_adds_two_numbers [/neotest-minitest/tests/minitest_examples/rails_unit_test.rb:8]: 171 | Expected: 4 172 | Actual: 5 173 | 174 | 175 | rails test Users/abry/src/nvim-neotest/neotest-minitest/tests/minitest_examples/rails_unit_test.rb:7 176 | 177 | RailsUnitTest#test_subtracts_two_numbers = 0.00 s = F 178 | 179 | 180 | Failure: 181 | RailsUnitTest#test_subtracts_two_numbers [/neotest-minitest/tests/minitest_examples/rails_unit_test.rb:11]: 182 | Expected: 1 183 | Actual: 2 184 | 185 | 186 | ]] 187 | it("parses the results correctly", function() 188 | local results = plugin._parse_test_output(output, { 189 | ["RailsUnitTest#test_adds_two_numbers"] = "testing", 190 | ["RailsUnitTest#test_subtracts_two_numbers"] = "testing2", 191 | }) 192 | 193 | assert.are.same({ 194 | ["testing"] = { status = "failed", errors = { { message = "Expected: 4\n Actual: 5", line = 7 } } }, 195 | ["testing2"] = { status = "failed", errors = { { message = "Expected: 1\n Actual: 2", line = 10 } } }, 196 | }, results) 197 | end) 198 | end) 199 | 200 | describe("multiple passing tests", function() 201 | local output = [[ 202 | RailsUnitTest#test_subtracts_two_numbers = 0.00 s = . 203 | RailsUnitTest#test_adds_two_numbers = 0.00 s = . 204 | ]] 205 | 206 | it("parses the results correctly", function() 207 | local results = plugin._parse_test_output(output, { 208 | ["RailsUnitTest#test_adds_two_numbers"] = "testing", 209 | ["RailsUnitTest#test_subtracts_two_numbers"] = "testing2", 210 | }) 211 | 212 | assert.are.same({ 213 | ["testing"] = { status = "passed" }, 214 | ["testing2"] = { status = "passed" }, 215 | }, results) 216 | end) 217 | end) 218 | end) 219 | end) 220 | -------------------------------------------------------------------------------- /tests/adapter/classic_spec.lua: -------------------------------------------------------------------------------- 1 | local plugin = require("neotest-minitest") 2 | local async = require("nio.tests") 3 | 4 | describe("Classic Test", function() 5 | describe("build_spec", function() 6 | async.it("should build a spec", function() 7 | local test_path = vim.loop.cwd() .. "/tests/minitest_examples/classic_test.rb" 8 | local tree = plugin.discover_positions(test_path) 9 | 10 | local spec = plugin.build_spec({ 11 | tree = tree, 12 | strategy = "dap", 13 | }) 14 | 15 | local expected_strategy = { 16 | args = { 17 | "-O", 18 | "--port", 19 | 62164, 20 | "-c", 21 | "-e", 22 | "cont", 23 | "--", 24 | "bundle", 25 | "exec", 26 | "ruby", 27 | "-Itest", 28 | vim.loop.cwd() .. "/tests/minitest_examples/classic_test.rb", 29 | "-v", 30 | }, 31 | bundle = "bundle", 32 | command = "rdbg", 33 | cwd = "${workspaceFolder}", 34 | localfs = true, 35 | name = "Neotest Debugger", 36 | port = 62164, 37 | request = "attach", 38 | type = "ruby", 39 | } 40 | 41 | assert.are.same(spec.strategy, expected_strategy) 42 | end) 43 | end) 44 | 45 | describe("discovers_positions", function() 46 | async.it("should discover the position of the test", function() 47 | local test_path = vim.loop.cwd() .. "/tests/minitest_examples/classic_test.rb" 48 | local positions = plugin.discover_positions(test_path):to_list() 49 | 50 | local expected_positions = { 51 | { 52 | id = test_path, 53 | name = "classic_test.rb", 54 | path = test_path, 55 | range = { 0, 0, 9, 0 }, 56 | type = "file", 57 | }, 58 | { 59 | { 60 | id = "./tests/minitest_examples/classic_test.rb::5", 61 | name = "ClassicTest", 62 | path = test_path, 63 | range = { 4, 0, 8, 3 }, 64 | type = "namespace", 65 | }, 66 | { 67 | { 68 | id = "./tests/minitest_examples/classic_test.rb::6", 69 | name = "test_addition", 70 | path = test_path, 71 | range = { 5, 2, 7, 5 }, 72 | type = "test", 73 | }, 74 | }, 75 | }, 76 | } 77 | 78 | assert.are.same(positions, expected_positions) 79 | end) 80 | end) 81 | 82 | describe("_parse_test_output", function() 83 | assert:set_parameter("TableFormatLevel", -1) 84 | 85 | describe("single assert_equal failure, output from file", function() 86 | local output_file_path = "tests/outputs/assert_equal_failure.txt" 87 | local f = assert(io.open(output_file_path, "r")) 88 | local output = f:read("*all") 89 | f.close() 90 | it("parses the results correctly", function() 91 | local results = plugin._parse_test_output(output, { ["UserInfoControllerTest#test_throwaway"] = "testing" }) 92 | 93 | assert.are.same({ 94 | ["testing"] = { status = "failed", errors = { { message = "Expected: 2\n Actual: 3", line = 20 } } }, 95 | }, results) 96 | end) 97 | end) 98 | 99 | describe("single failing test", function() 100 | local output = [[ 101 | ClassicTest#test_addition = 0.00 s = F 102 | 103 | 104 | Failure: 105 | ClassicTest#test_addition [/neotest-minitest/tests/minitest_examples/classic_test.rb:7]: 106 | Expected: 3 107 | Actual: 2 108 | 109 | 110 | ]] 111 | 112 | it("parses the results correctly", function() 113 | local results = plugin._parse_test_output(output, { ["ClassicTest#test_addition"] = "testing" }) 114 | 115 | assert.are.same({ 116 | ["testing"] = { status = "failed", errors = { { message = "Expected: 3\n Actual: 2", line = 6 } } }, 117 | }, results) 118 | end) 119 | end) 120 | 121 | describe("single passing test", function() 122 | local output = [[ 123 | ClassicTest#test_addition = 0.00 s = . 124 | ]] 125 | 126 | it("parses the results correctly", function() 127 | local results = plugin._parse_test_output(output, { ["ClassicTest#test_addition"] = "testing" }) 128 | 129 | assert.are.same({ 130 | ["testing"] = { status = "passed" }, 131 | }, results) 132 | end) 133 | end) 134 | 135 | describe("single error test", function() 136 | local output = [[ 137 | ClassicTest#test_error = 0.00 s = E 138 | 139 | Finished in 0.000627s, 1594.8960 runs/s, 0.0000 assertions/s. 140 | 141 | 1) Error: 142 | ClassicTest#test_error: 143 | NameError: uninitialized constant ClassicTest::Unknown 144 | 145 | assert_equal false, Unknown.function 146 | ^^^^^^^ 147 | /Users/abry/src/nvim-neotest/neotest-minitest/tests/minitest_examples/classic_test.rb:9:in `test_error' 148 | ]] 149 | 150 | it("parses the results correctly", function() 151 | local results = plugin._parse_test_output(output, { ["ClassicTest#test_error"] = "testing" }) 152 | assert.are.same({ 153 | ["testing"] = { 154 | status = "failed", 155 | errors = { 156 | { 157 | message = "NameError: uninitialized constant ClassicTest::Unknown", 158 | line = 8, 159 | }, 160 | }, 161 | }, 162 | }, results) 163 | end) 164 | end) 165 | 166 | describe("multiple error tests", function() 167 | local output = [[ 168 | ClassicTest#test_error = 0.00 s = E 169 | ClassicTest#test_error2 = 0.00 s = E 170 | Run options: -v --seed 44295 171 | 172 | 173 | 1) Error: 174 | ClassicTest#test_error: 175 | NameError: uninitialized constant ClassicTest::Unknown 176 | 177 | assert_equal false, Unknown.function 178 | ^^^^^^^ 179 | /Users/abry/src/nvim-neotest/neotest-minitest/tests/minitest_examples/classic_test.rb:7:in `test_error' 180 | 181 | 2) Error: 182 | ClassicTest#test_error2: 183 | NameError: uninitialized constant ClassicTest::Unknown 184 | 185 | assert_equal false, Unknown.function 186 | ^^^^^^^ 187 | /Users/abry/src/nvim-neotest/neotest-minitest/tests/minitest_examples/classic_test.rb:11:in `test_error2' 188 | ]] 189 | 190 | it("parses the results correctly", function() 191 | local results = plugin._parse_test_output(output, { 192 | ["ClassicTest#test_error"] = "testing_error", 193 | ["ClassicTest#test_error2"] = "testing_error2", 194 | }) 195 | 196 | assert.are.same({ 197 | ["testing_error"] = { 198 | status = "failed", 199 | errors = { 200 | { 201 | message = "NameError: uninitialized constant ClassicTest::Unknown", 202 | line = 6, 203 | }, 204 | }, 205 | }, 206 | ["testing_error2"] = { 207 | status = "failed", 208 | errors = { 209 | { 210 | message = "NameError: uninitialized constant ClassicTest::Unknown", 211 | line = 10, 212 | }, 213 | }, 214 | }, 215 | }, results) 216 | end) 217 | end) 218 | 219 | describe("failing and passing tests", function() 220 | local output = [[ 221 | ClassicTest#test_subtraction = 0.00 s = F 222 | 223 | 224 | Failure: 225 | ClassicTest#test_subtraction [/neotest-minitest/tests/minitest_examples/classic_test.rb:10]: 226 | Expected: 1 227 | Actual: 0 228 | 229 | 230 | rails test Users/abry/src/nvim-neotest/neotest-minitest/tests/minitest_examples/classic_test.rb:9 231 | 232 | ClassicTest#test_addition = 0.00 s = . 233 | ]] 234 | it("parses the results correctly", function() 235 | local results = plugin._parse_test_output(output, { 236 | ["ClassicTest#test_subtraction"] = "testing_subtraction", 237 | ["ClassicTest#test_addition"] = "testing_addition", 238 | }) 239 | 240 | assert.are.same({ 241 | ["testing_subtraction"] = { 242 | status = "failed", 243 | errors = { { message = "Expected: 1\n Actual: 0", line = 9 } }, 244 | }, 245 | ["testing_addition"] = { status = "passed" }, 246 | }, results) 247 | end) 248 | end) 249 | end) 250 | 251 | describe("multiple failing tests", function() 252 | output = [[ 253 | ClassicTest#test_addition = 0.00 s = F 254 | 255 | 256 | Failure: 257 | ClassicTest#test_addition [/neotest-minitest/tests/minitest_examples/classic_test.rb:7]: 258 | Expected: 5 259 | Actual: 2 260 | 261 | 262 | rails test Users/abry/src/nvim-neotest/neotest-minitest/tests/minitest_examples/classic_test.rb:6 263 | 264 | ClassicTest#test_subtraction = 0.00 s = F 265 | 266 | 267 | Failure: 268 | ClassicTest#test_subtraction [/neotest-minitest/tests/minitest_examples/classic_test.rb:10]: 269 | Expected: 1 270 | Actual: 0 271 | 272 | 273 | rails test Users/abry/src/nvim-neotest/neotest-minitest/tests/minitest_examples/classic_test.rb:9 274 | ]] 275 | 276 | it("parses the results correctly", function() 277 | local results = plugin._parse_test_output(output, { 278 | ["ClassicTest#test_addition"] = "testing_addition", 279 | ["ClassicTest#test_subtraction"] = "testing_subtraction", 280 | }) 281 | 282 | assert.are.same({ 283 | ["testing_addition"] = { 284 | status = "failed", 285 | errors = { { message = "Expected: 5\n Actual: 2", line = 6 } }, 286 | }, 287 | ["testing_subtraction"] = { 288 | status = "failed", 289 | errors = { { message = "Expected: 1\n Actual: 0", line = 9 } }, 290 | }, 291 | }, results) 292 | end) 293 | end) 294 | 295 | describe("multiple passing tests", function() 296 | local output = [[ 297 | ClassicTest#test_subtraction = 0.00 s = . 298 | ClassicTest#test_addition = 0.00 s = . 299 | ]] 300 | it("parses the results correctly", function() 301 | local results = plugin._parse_test_output(output, { 302 | ["ClassicTest#test_subtraction"] = "testing_subtraction", 303 | ["ClassicTest#test_addition"] = "testing_addition", 304 | }) 305 | 306 | assert.are.same({ 307 | ["testing_subtraction"] = { status = "passed" }, 308 | ["testing_addition"] = { status = "passed" }, 309 | }, results) 310 | end) 311 | end) 312 | end) 313 | -------------------------------------------------------------------------------- /tests/adapter/spec_spec.lua: -------------------------------------------------------------------------------- 1 | local plugin = require("neotest-minitest") 2 | local async = require("nio.tests") 3 | 4 | describe("Spec Test", function() 5 | assert:set_parameter("TableFormatLevel", -1) 6 | describe("discover_positions", function() 7 | async.it("should discover the position for the tests", function() 8 | local test_path = vim.loop.cwd() .. "/tests/minitest_examples/spec_test.rb" 9 | local positions = plugin.discover_positions(test_path):to_list() 10 | local expected_positions = { 11 | { 12 | id = test_path, 13 | name = "spec_test.rb", 14 | path = test_path, 15 | range = { 0, 0, 18, 0 }, 16 | type = "file", 17 | }, 18 | { 19 | { 20 | id = "./tests/minitest_examples/spec_test.rb::6", 21 | name = "SpecTest", 22 | path = test_path, 23 | range = { 5, 0, 17, 3 }, 24 | type = "namespace", 25 | }, 26 | { 27 | { 28 | id = "./tests/minitest_examples/spec_test.rb::7", 29 | name = "addition", 30 | path = test_path, 31 | range = { 6, 2, 10, 5 }, 32 | type = "namespace", 33 | }, 34 | { 35 | { 36 | id = "./tests/minitest_examples/spec_test.rb::8", 37 | name = "adds two numbers", 38 | path = test_path, 39 | range = { 7, 4, 9, 7 }, 40 | type = "test", 41 | }, 42 | }, 43 | }, 44 | { 45 | { 46 | id = "./tests/minitest_examples/spec_test.rb::13", 47 | name = "subtraction", 48 | path = test_path, 49 | range = { 12, 2, 16, 5 }, 50 | type = "namespace", 51 | }, 52 | { 53 | { 54 | id = "./tests/minitest_examples/spec_test.rb::14", 55 | name = "subtracts two numbers", 56 | path = test_path, 57 | range = { 13, 4, 15, 7 }, 58 | type = "test", 59 | }, 60 | }, 61 | }, 62 | }, 63 | } 64 | 65 | assert.are.same(positions, expected_positions) 66 | end) 67 | 68 | async.it("should discover the position for the tests, minispec-test-rails variant", function() 69 | local test_path = vim.loop.cwd() .. "/tests/minitest_examples/rails_spec_test.rb" 70 | local positions = plugin.discover_positions(test_path):to_list() 71 | local expected_positions = { 72 | { 73 | id = test_path, 74 | name = "rails_spec_test.rb", 75 | path = test_path, 76 | range = { 0, 0, 18, 0 }, 77 | type = "file", 78 | }, 79 | { 80 | { 81 | id = "./tests/minitest_examples/rails_spec_test.rb::6", 82 | name = "RailsSpecTest", 83 | path = test_path, 84 | range = { 5, 0, 17, 3 }, 85 | type = "namespace", 86 | }, 87 | { 88 | { 89 | id = "./tests/minitest_examples/rails_spec_test.rb::7", 90 | name = "addition", 91 | path = test_path, 92 | range = { 6, 2, 10, 5 }, 93 | type = "namespace", 94 | }, 95 | { 96 | { 97 | id = "./tests/minitest_examples/rails_spec_test.rb::8", 98 | name = "adds two numbers", 99 | path = test_path, 100 | range = { 7, 4, 9, 7 }, 101 | type = "test", 102 | }, 103 | }, 104 | }, 105 | { 106 | { 107 | id = "./tests/minitest_examples/rails_spec_test.rb::13", 108 | name = "subtraction", 109 | path = test_path, 110 | range = { 12, 2, 16, 5 }, 111 | type = "namespace", 112 | }, 113 | { 114 | { 115 | id = "./tests/minitest_examples/rails_spec_test.rb::14", 116 | name = "subtracts two numbers", 117 | path = test_path, 118 | range = { 13, 4, 15, 7 }, 119 | type = "test", 120 | }, 121 | }, 122 | }, 123 | }, 124 | } 125 | 126 | assert.are.same(positions, expected_positions) 127 | end) 128 | end) 129 | 130 | describe("_parse_test_output", function() 131 | describe("single failing test", function() 132 | local output = [[ 133 | SpecTest::addition#test_0001_adds two numbers = 0.00 s = F 134 | 135 | Failure: 136 | SpecTest::addition#test_0001_adds two numbers [tests/minitest_examples/spec_test.rb:9]: 137 | Expected: 4 138 | Actual: 5 139 | 140 | 141 | ]] 142 | it("parses the results correctly", function() 143 | local results = 144 | plugin._parse_test_output(output, { ["SpecTest::addition#test_0001_adds two numbers"] = "testing" }) 145 | 146 | assert.are.same( 147 | { ["testing"] = { status = "failed", errors = { { message = "Expected: 4\n Actual: 5", line = 8 } } } }, 148 | results 149 | ) 150 | end) 151 | end) 152 | 153 | describe("single passing test with ruby error", function() 154 | local output = [[ 155 | Traceback (most recent call last): 156 | 1: from tests/minitest_examples/rails_unit_erroring_test.rb:1:in `
' 157 | tests/minitest_examples/rails_unit_erroring_test.rb:1:in `require': cannot load such file -- non_exising_file (LoadError) 158 | ]] 159 | 160 | it("parses the results correctly", function() 161 | local results = plugin._parse_test_output(output, { ["RailsUnitErroringTest#test_addition"] = "testing" }) 162 | 163 | assert.are.same({ 164 | ["testing"] = { 165 | status = "failed", 166 | errors = { 167 | { message = "in `require': cannot load such file -- non_exising_file (LoadError)", line = 0 }, 168 | }, 169 | }, 170 | }, results) 171 | end) 172 | end) 173 | 174 | describe("multiple tests with ruby error", function() 175 | local output = [[ 176 | Traceback (most recent call last): 177 | 1: from tests/minitest_examples/rails_unit_erroring_test.rb:1:in `
' 178 | tests/minitest_examples/rails_unit_erroring_test.rb:1:in `require': cannot load such file -- non_exising_file (LoadError) 179 | ]] 180 | 181 | it("parses the results correctly", function() 182 | local results = plugin._parse_test_output(output, { 183 | ["RailsUnitErroringTest#test_addition"] = "testing", 184 | ["SpecTest#test_subtracts_two_numbers"] = "testing1", 185 | }) 186 | 187 | assert.are.same({ 188 | ["testing"] = { 189 | status = "failed", 190 | errors = { 191 | { message = "in `require': cannot load such file -- non_exising_file (LoadError)", line = 0 }, 192 | }, 193 | }, 194 | ["testing1"] = { 195 | status = "failed", 196 | errors = { 197 | { message = "in `require': cannot load such file -- non_exising_file (LoadError)", line = 0 }, 198 | }, 199 | }, 200 | }, results) 201 | end) 202 | end) 203 | 204 | describe("single passing test", function() 205 | local output = [[ 206 | SpecTest::subtraction#test_0001_subtracts two numbers = 0.00 s = . 207 | ]] 208 | 209 | it("parses the results correctly", function() 210 | local results = 211 | plugin._parse_test_output(output, { ["SpecTest::subtraction#test_0001_subtracts two numbers"] = "testing" }) 212 | 213 | assert.are.same({ ["testing"] = { status = "passed" } }, results) 214 | end) 215 | end) 216 | 217 | describe("failing and passing tests", function() 218 | local output = [[ 219 | SpecTest#test_subtracts_two_numbers = 0.00 s = . 220 | SpecTest#test_adds_two_numbers = 0.00 s = F 221 | 222 | 223 | Failure: 224 | SpecTest#test_adds_two_numbers [/neotest-minitest/tests/minitest_examples/spec_test.rb:8]: 225 | Expected: 4 226 | Actual: 5 227 | 228 | 229 | ]] 230 | 231 | it("parses the results correctly", function() 232 | local results = plugin._parse_test_output(output, { 233 | ["SpecTest#test_adds_two_numbers"] = "testing", 234 | ["SpecTest#test_subtracts_two_numbers"] = "testing2", 235 | }) 236 | 237 | assert.are.same({ 238 | ["testing"] = { status = "failed", errors = { { message = "Expected: 4\n Actual: 5", line = 7 } } }, 239 | ["testing2"] = { status = "passed" }, 240 | }, results) 241 | end) 242 | end) 243 | 244 | describe("multiple failing tests", function() 245 | local output = [[ 246 | SpecTest#test_adds_two_numbers = 0.00 s = F 247 | 248 | 249 | Failure: 250 | SpecTest#test_adds_two_numbers [/neotest-minitest/tests/minitest_examples/spec_test.rb:8]: 251 | Expected: 4 252 | Actual: 5 253 | 254 | 255 | rails test Users/abry/src/nvim-neotest/neotest-minitest/tests/minitest_examples/spec_test.rb:7 256 | 257 | SpecTest#test_subtracts_two_numbers = 0.00 s = F 258 | 259 | 260 | Failure: 261 | SpecTest#test_subtracts_two_numbers [/neotest-minitest/tests/minitest_examples/spec_test.rb:11]: 262 | Expected: 1 263 | Actual: 2 264 | 265 | 266 | ]] 267 | it("parses the results correctly", function() 268 | local results = plugin._parse_test_output(output, { 269 | ["SpecTest#test_adds_two_numbers"] = "testing", 270 | ["SpecTest#test_subtracts_two_numbers"] = "testing2", 271 | }) 272 | 273 | assert.are.same({ 274 | ["testing"] = { status = "failed", errors = { { message = "Expected: 4\n Actual: 5", line = 7 } } }, 275 | ["testing2"] = { status = "failed", errors = { { message = "Expected: 1\n Actual: 2", line = 10 } } }, 276 | }, results) 277 | end) 278 | end) 279 | 280 | describe("multiple passing tests", function() 281 | local output = [[ 282 | SpecTest#test_subtracts_two_numbers = 0.00 s = . 283 | SpecTest#test_adds_two_numbers = 0.00 s = . 284 | ]] 285 | 286 | it("parses the results correctly", function() 287 | local results = plugin._parse_test_output(output, { 288 | ["SpecTest#test_adds_two_numbers"] = "testing", 289 | ["SpecTest#test_subtracts_two_numbers"] = "testing2", 290 | }) 291 | 292 | assert.are.same({ 293 | ["testing"] = { status = "passed" }, 294 | ["testing2"] = { status = "passed" }, 295 | }, results) 296 | end) 297 | end) 298 | end) 299 | end) 300 | -------------------------------------------------------------------------------- /lua/neotest-minitest/init.lua: -------------------------------------------------------------------------------- 1 | local lib = require("neotest.lib") 2 | local logger = require("neotest.logging") 3 | local async = require("neotest.async") 4 | 5 | local config = require("neotest-minitest.config") 6 | local utils = require("neotest-minitest.utils") 7 | 8 | ---@class neotest.Adapter 9 | ---@field name string 10 | local NeotestAdapter = { name = "neotest-minitest" } 11 | 12 | ---Find the project root directory given a current directory to work from. 13 | ---Should no root be found, the adapter can still be used in a non-project context if a test file matches. 14 | ---@async 15 | ---@param dir string @Directory to treat as cwd 16 | ---@return string | nil @Absolute root dir of test suite 17 | NeotestAdapter.root = lib.files.match_root_pattern("Gemfile", ".gitignore") 18 | 19 | ---@async 20 | ---@param file_path string 21 | ---@return boolean 22 | function NeotestAdapter.is_test_file(file_path) 23 | return vim.endswith(file_path, "_test.rb") or string.match(file_path, "/test_.+%.rb$") ~= nil 24 | end 25 | 26 | ---Filter directories when searching for test files 27 | ---@async 28 | ---@param name string Name of directory 29 | ---@param rel_path string Path to directory, relative to root 30 | ---@param root string Root directory of project 31 | ---@return boolean 32 | function NeotestAdapter.filter_dir(name, rel_path, root) 33 | return name ~= "vendor" 34 | end 35 | 36 | ---Given a file path, parse all the tests within it. 37 | ---@async 38 | ---@param file_path string Absolute file path 39 | ---@return neotest.Tree | nil 40 | function NeotestAdapter.discover_positions(file_path) 41 | local query = [[ 42 | ; Classes that inherit from Minitest::Test 43 | (( 44 | class 45 | name: (constant) @namespace.name 46 | (superclass (scope_resolution) @superclass (#match? @superclass "^Minitest::Test")) 47 | )) @namespace.definition 48 | 49 | ; System tests that inherit from ApplicationSystemTestCase 50 | (( 51 | class 52 | name: [ 53 | (constant) @namespace.name 54 | (scope_resolution scope: (constant) name: (constant) @namespace.name) 55 | ] 56 | (superclass) @superclass (#match? @superclass "(ApplicationSystemTestCase)$" ) 57 | )) @namespace.definition 58 | 59 | ; Methods that begin with test_ 60 | (( 61 | method 62 | name: (identifier) @test.name (#match? @test.name "^test_") 63 | )) @test.definition 64 | 65 | ; rails unit classes 66 | (( 67 | class 68 | name: [ 69 | (constant) @namespace.name 70 | (scope_resolution scope: (constant) name: (constant) @namespace.name) 71 | ] 72 | (superclass (scope_resolution) @superclass (#match? @superclass "(::IntegrationTest|::TestCase|::SystemTestCase)$")) 73 | )) @namespace.definition 74 | 75 | (( 76 | call 77 | method: (identifier) @func_name (#match? @func_name "^(describe|context)$") 78 | arguments: (argument_list (string (string_content) @namespace.name)) 79 | )) @namespace.definition 80 | 81 | (( 82 | call 83 | method: (identifier) @namespace.name (#match? @namespace.name "^(describe|context)$") 84 | . 85 | block: (_) 86 | )) @namespace.definition 87 | 88 | (( 89 | call 90 | method: (identifier) @func_name (#match? @func_name "^(test)$") 91 | arguments: (argument_list (string (string_content) @test.name)) 92 | )) @test.definition 93 | 94 | (( 95 | call 96 | method: (identifier) @func_name (#match? @func_name "^(it)$") 97 | arguments: (argument_list (string (string_content) @test.name)) 98 | )) @test.definition 99 | ]] 100 | 101 | return lib.treesitter.parse_positions(file_path, query, { 102 | nested_tests = true, 103 | require_namespaces = true, 104 | position_id = "require('neotest-minitest.utils').generate_treesitter_id", 105 | }) 106 | end 107 | 108 | ---@param args neotest.RunArgs 109 | ---@return nil | neotest.RunSpec | neotest.RunSpec[] 110 | function NeotestAdapter.build_spec(args) 111 | local script_args = {} 112 | local position = args.tree:data() 113 | local results_path = config.results_path() 114 | local spec_path = config.transform_spec_path(position.path) 115 | 116 | local name_mappings = utils.get_mappings(args.tree) 117 | 118 | local function run_by_filename() 119 | table.insert(script_args, spec_path) 120 | end 121 | 122 | local function run_by_name() 123 | local full_spec_name = utils.full_spec_name(args.tree) 124 | local full_test_name = utils.escaped_full_test_name(args.tree) 125 | table.insert(script_args, spec_path) 126 | table.insert(script_args, "--name") 127 | -- https://chriskottom.com/articles/command-line-flags-for-minitest-in-the-raw/ 128 | table.insert(script_args, "/^" .. full_spec_name .. "|" .. full_test_name .. "$/") 129 | end 130 | 131 | local function run_dir() 132 | local tree = args.tree 133 | local root = tree:root():data().path 134 | 135 | -- This emulates an combination of Rake::TestTask with loader=:direct and 136 | -- rake_test_loader 137 | table.insert(script_args, "-e") 138 | table.insert(script_args, "while (f = ARGV.shift) != '--'; require f; end") 139 | 140 | -- Instruct Ruby to stop parsing options 141 | table.insert(script_args, "--") 142 | 143 | for _, node in tree:iter_nodes() do 144 | if node:data().type == "file" then 145 | local path = node:data().path 146 | table.insert(script_args, path) 147 | end 148 | end 149 | 150 | -- Mark the end of test files 151 | table.insert(script_args, "--") 152 | end 153 | 154 | local function dap_strategy(command) 155 | local port = math.random(49152, 65535) 156 | port = config.port or port 157 | 158 | local rdbg_args = { 159 | "-O", 160 | "--port", 161 | port, 162 | "-c", 163 | "-e", 164 | "cont", 165 | "--", 166 | } 167 | 168 | for i = 1, #command do 169 | rdbg_args[#rdbg_args + 1] = command[i] 170 | end 171 | 172 | return { 173 | name = "Neotest Debugger", 174 | type = "ruby", 175 | bundle = "bundle", 176 | localfs = true, 177 | request = "attach", 178 | args = rdbg_args, 179 | command = "rdbg", 180 | cwd = "${workspaceFolder}", 181 | port = port, 182 | } 183 | end 184 | 185 | if position.type == "file" then run_by_filename() end 186 | 187 | if position.type == "test" or position.type == "namespace" then run_by_name() end 188 | 189 | if position.type == "dir" then run_dir() end 190 | 191 | local command = vim.tbl_flatten({ 192 | config.get_test_cmd(), 193 | script_args, 194 | "-v", 195 | }) 196 | 197 | if args.strategy == "dap" then 198 | return { 199 | command = command, 200 | context = { 201 | results_path = results_path, 202 | pos_id = position.id, 203 | name_mappings = name_mappings, 204 | }, 205 | strategy = dap_strategy(command), 206 | } 207 | else 208 | return { 209 | cwd = nil, 210 | command = command, 211 | context = { 212 | results_path = results_path, 213 | pos_id = position.id, 214 | name_mappings = name_mappings, 215 | }, 216 | } 217 | end 218 | end 219 | 220 | local iter_test_output_error = function(output) 221 | local header_pattern = "Failure:%s*" 222 | local filepath_pattern = "%s+%[([^%]]+)]:%s*" 223 | local result_pattern = "Expected:%s*(.-)%s*Actual:%s*(.-)%s" 224 | 225 | -- keep track of last test error position 226 | local last_pos = 0 227 | 228 | return function() 229 | -- find error header 230 | local h_start, h_end = string.find(output, header_pattern, last_pos) 231 | if h_start == nil or h_end == nil then return nil, nil, nil, nil end 232 | 233 | -- find file path 234 | local f_start, f_end = string.find(output, filepath_pattern, h_end) 235 | if f_start == nil or f_end == nil then return nil, nil, nil, nil end 236 | 237 | -- extract file path 238 | local filepath = string.match(output, filepath_pattern, f_start) 239 | 240 | -- extract test name 241 | local test_name = string.sub(output, h_end + 1, f_start - 1) 242 | 243 | -- find expected and result 244 | local expected, actual = string.match(output, result_pattern, f_end) 245 | 246 | -- keep track of last test error position 247 | last_pos = f_end 248 | 249 | return test_name, filepath, expected, actual 250 | end 251 | end 252 | 253 | local iter_test_output_status = function(output) 254 | local pattern = "%s*=%s*[%d.]+%s*s%s*=%s*([FE.])" 255 | 256 | -- keep track of last test result position 257 | local last_pos = 0 258 | 259 | return function() 260 | -- find test result 261 | local r_start, r_end = string.find(output, pattern, last_pos) 262 | if r_start == nil or r_end == nil then return nil, nil end 263 | 264 | -- extract status from test results 265 | local test_status = string.match(output, pattern, r_start) 266 | 267 | -- find test name 268 | -- 269 | -- iterate backwards through output until we find a newline or start of output. 270 | local n_start = 0 271 | for i = r_start, 0, -1 do 272 | if string.sub(output, i, i) == "\n" then 273 | n_start = i + 1 274 | break 275 | end 276 | end 277 | local test_name = string.sub(output, n_start, r_start - 1) 278 | 279 | -- keep track of last test result position 280 | last_pos = r_end 281 | 282 | return test_name, test_status 283 | end 284 | end 285 | 286 | function NeotestAdapter._parse_test_output(output, name_mappings) 287 | local results = {} 288 | local error_pattern = "Error:%s*([%w:#_]+):%s*(.-)\n[%w%W]-%.rb:(%d+):" 289 | local traceback_pattern = "(%d+:[^:]+:%d+:in `[^']+')%s+([^:]+):(%d+):(in `[^']+':[^\n]+)" 290 | 291 | for _, _, line_str, message in string.gmatch(output, traceback_pattern) do 292 | local line = tonumber(line_str) 293 | for _, pos_id in pairs(name_mappings) do 294 | results[pos_id] = { 295 | status = "failed", 296 | errors = { 297 | { 298 | message = message, 299 | line = line - 1, 300 | }, 301 | }, 302 | } 303 | end 304 | end 305 | 306 | for test_name, status in iter_test_output_status(output) do 307 | local pos_id = name_mappings[test_name] 308 | if not pos_id then 309 | test_name = utils.replace_module_namespace(test_name) 310 | if name_mappings[test_name] then pos_id = name_mappings[test_name] end 311 | end 312 | 313 | if pos_id then results[pos_id] = { 314 | status = status == "." and "passed" or "failed", 315 | } end 316 | end 317 | 318 | for test_name, filepath, expected, actual in iter_test_output_error(output) do 319 | local message = string.format("Expected: %s\n Actual: %s", expected, actual) 320 | 321 | local pos_id = name_mappings[test_name] 322 | if not pos_id then 323 | test_name = utils.replace_module_namespace(test_name) 324 | pos_id = name_mappings[test_name] 325 | end 326 | 327 | local line = tonumber(string.match(filepath, ":(%d+)$")) 328 | if results[pos_id] then 329 | results[pos_id].status = "failed" 330 | results[pos_id].errors = { 331 | { 332 | message = message, 333 | line = line - 1, 334 | }, 335 | } 336 | end 337 | end 338 | 339 | for test_name, message, line_str in string.gmatch(output, error_pattern) do 340 | test_name = utils.replace_module_namespace(test_name) 341 | local line = tonumber(line_str) 342 | local pos_id = name_mappings[test_name] 343 | if results[pos_id] then 344 | results[pos_id].status = "failed" 345 | results[pos_id].errors = { 346 | { 347 | message = message, 348 | line = line - 1, -- neovim lines are 0 indexed 349 | }, 350 | } 351 | end 352 | end 353 | 354 | return results 355 | end 356 | 357 | ---@async 358 | ---@param spec neotest.RunSpec 359 | ---@param result neotest.StrategyResult 360 | ---@param tree neotest.Tree 361 | ---@return table 362 | function NeotestAdapter.results(spec, result, tree) 363 | local success, output = pcall(lib.files.read, result.output) 364 | if not success then 365 | logger.error("neotest-minitest: could not read output: " .. output) 366 | return {} 367 | end 368 | 369 | output = utils.strip_ansi_escape_codes(output) 370 | local results = NeotestAdapter._parse_test_output(output, spec.context.name_mappings) 371 | 372 | return results 373 | end 374 | 375 | local is_callable = function(obj) 376 | return type(obj) == "function" or (type(obj) == "table" and obj.__call) 377 | end 378 | 379 | setmetatable(NeotestAdapter, { 380 | __call = function(_, opts) 381 | if is_callable(opts.test_cmd) then 382 | config.get_test_cmd = opts.test_cmd 383 | elseif opts.test_cmd then 384 | config.get_test_cmd = function() 385 | return opts.test_cmd 386 | end 387 | end 388 | if is_callable(opts.transform_spec_path) then 389 | config.transform_spec_path = opts.transform_spec_path 390 | elseif opts.transform_spec_path then 391 | config.transform_spec_path = function() 392 | return opts.transform_spec_path 393 | end 394 | end 395 | if is_callable(opts.results_path) then 396 | config.results_path = opts.results_path 397 | elseif opts.results_path then 398 | config.results_path = function() 399 | return opts.results_path 400 | end 401 | end 402 | return NeotestAdapter 403 | end, 404 | }) 405 | 406 | return NeotestAdapter 407 | --------------------------------------------------------------------------------