Skip to content

Commit

Permalink
Add JSON test runner for Minitest
Browse files Browse the repository at this point in the history
  • Loading branch information
andyw8 committed Mar 5, 2025
1 parent 6f93d7b commit 3295ec2
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 8 deletions.
5 changes: 4 additions & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Sorbet/TrueSigil:
- "lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb"
- "lib/ruby_lsp/load_sorbet.rb"
- "lib/ruby_lsp/scripts/compose_bundle.rb"
- "lib/ruby_lsp/test_unit_test_runner.rb"
Exclude:
- "**/*.rake"
- "lib/**/*.rb"
Expand All @@ -67,7 +68,9 @@ Sorbet/StrictSigil:
- "lib/ruby_lsp/load_sorbet.rb"
- "lib/ruby_lsp/scripts/compose_bundle.rb"
- "lib/ruby_lsp/test_helper.rb"
- "lib/ruby_lsp/test_unit_test_runner.rb" # runs as part of host app where sorbet-runtime may not be available
# runs as part of host app where sorbet-runtime may not be available. Using RBS signatures we can get to
# `typed: true` but not yet `typed: strict`
- "lib/ruby_lsp/test_unit_test_runner.rb"

Layout/ClassStructure:
Enabled: true
Expand Down
110 changes: 110 additions & 0 deletions lib/ruby_lsp/ruby_lsp_reporter_plugin.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# typed: strict
# frozen_string_literal: true

# require_relative "../../ruby_lsp/test_reporter"
require_relative "test_reporter"

# RUBYOPT="-r./lib/ruby_lsp/ruby_lsp_reporter_plugin.rb" bundle exec ruby -Itest test/fixtures/minitest_example.rb

require "minitest"

module Minitest
module Reporters
class RubyLspReporter < ::Minitest::AbstractReporter
class << self
#: (Hash[untyped, untyped]) -> void
def minitest_plugin_init(_options)
Minitest.reporter.reporters = [RubyLspReporter.new]
end
end

#: (singleton(Minitest::Test) test_class, String method_name) -> void
def prerecord(test_class, method_name)
uri = uri_from_test_class(test_class, method_name)
return unless uri

RubyLsp::TestReporter.start_test(
id: "#{test_class.name}##{method_name}",
uri: uri,
)
end

#: (Minitest::Result result) -> void
def record(result)
super

if result.error?
record_error(result)
elsif result.passed?
record_pass(result)
elsif result.skipped?
record_skip(result)
elsif result.failure
record_fail(result)
end
end

#: (Minitest::Result result) -> void
def record_pass(result)
RubyLsp::TestReporter.record_pass(
id: id_from_result(result),
uri: uri_from_result(result),
)
end

#: (Minitest::Result result) -> void
def record_skip(result)
RubyLsp::TestReporter.record_skip(
id: id_from_result(result),
message: result.failure.message,
uri: uri_from_result(result),
)
end

#: (Minitest::Result result) -> void
def record_fail(result)
RubyLsp::TestReporter.record_fail(
id: id_from_result(result),
message: result.failure.message,
uri: uri_from_result(result),
)
end

#: (Minitest::Result result) -> void
def record_error(result)
RubyLsp::TestReporter.record_error(
id: id_from_result(result),
uri: uri_from_result(result),
message: result.failures.first.message,
)
end

private

#: (Minitest::Result result) -> String
def id_from_result(result)
"#{result.klass}##{result.name}"
end

#: (Minitest::Result result) -> URI::Generic
def uri_from_result(result)
file = result.source_location[0]
absolute_path = File.expand_path(file, Dir.pwd)
URI::Generic.from_path(path: absolute_path)
end

#: (singleton(Minitest::Test) test_class, String method_name) -> URI::Generic?
def uri_from_test_class(test_class, method_name)
file, _line = test_class.instance_method(method_name).source_location
return unless file

return if file.start_with?("(eval at ") # test is dynamically defined

absolute_path = File.expand_path(file, Dir.pwd)
URI::Generic.from_path(path: absolute_path)
end
end
end
end

Minitest.extensions << Minitest::Reporters::RubyLspReporter
2 changes: 1 addition & 1 deletion lib/ruby_lsp/test_reporter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def record_skip(id:, message:, uri:)
send_message("skip", params)
end

#: (id: String, message: String?, uri: String) -> void
#: (id: String, message: String?, uri: URI::Generic) -> void
def record_error(id:, message:, uri:)
params = {
id: id,
Expand Down
25 changes: 25 additions & 0 deletions test/fixtures/minitest_example.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

require "test_helper"

# We are only testing the output of the runner, there's no need for to be random.
Minitest::Test.i_suck_and_my_tests_are_order_dependent!

class Sample < Minitest::Test
def test_that_passes
assert_equal(1, 1)
assert_equal(2, 2)
end

def test_that_fails
assert_equal(1, 2)
end

def test_that_is_pending
skip("pending test")
end

def test_that_raises
raise "oops"
end
end
104 changes: 104 additions & 0 deletions test/minitest_test_runner_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# typed: true
# frozen_string_literal: true

require "test_helper"

module RubyLsp
class MinitestTestRunnerTest < Minitest::Test
def test_minitest_output
plugin_path = "lib/ruby_lsp/ruby_lsp_reporter_plugin.rb"
env = { "RUBYOPT" => "-r./#{plugin_path}" }
_stdin, stdout, _stderr, _wait_thr = T.unsafe(Open3).popen3(
env,
"bundle",
"exec",
"ruby",
"-Itest",
"test/fixtures/minitest_example.rb",
)
stdout.binmode
stdout.sync = true

actual = parse_output(stdout)

uri = URI::Generic.from_path(path: "#{Dir.pwd}/test/fixtures/minitest_example.rb").to_s
expected = [
{
"method" => "start",
"params" => {
"id" => "Sample#test_that_fails",
"uri" => uri,
},
},
{
"method" => "fail",
"params" => {
"id" => "Sample#test_that_fails",
"message" => "--- expected\n+++ actual\n@@ -1 +1 @@\n-1\n+2\n",
"uri" => uri,
},
},
{
"method" => "start",
"params" => {
"id" => "Sample#test_that_is_pending",
"uri" => uri,
},
},
{
"method" => "skip",
"params" => {
"id" => "Sample#test_that_is_pending",
"message" => "pending test",
"uri" => uri,
},
},
{
"method" => "start",
"params" => {
"id" => "Sample#test_that_passes",
"uri" => uri,
},
},
{
"method" => "pass",
"params" => {
"id" => "Sample#test_that_passes",
"uri" => uri,
},
},
{
"method" => "start",
"params" => {
"id" => "Sample#test_that_raises",
"uri" => uri,
},
},
{
"method" => "error",
"params" => {
"id" => "Sample#test_that_raises",
"message" => "RuntimeError: oops\n test/fixtures/minitest_example.rb:23:in 'Sample#test_that_raises'",
"uri" => uri,
},
},
]
assert_equal(8, actual.size)
assert_equal(expected, actual)
end

private

def parse_output(output)
result = []
while (headers = output.gets("\r\n\r\n"))
content_length = headers[/Content-Length: (\d+)/i, 1]
flunk("Error reading response") unless content_length
data = output.read(Integer(content_length))
json = JSON.parse(T.must(data))
result << json
end
result
end
end
end
13 changes: 7 additions & 6 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@
require "debug/config"
DEBUGGER__::CONFIG[:skip_path] = Array(DEBUGGER__::CONFIG[:skip_path]) + SORBET_PATHS

minitest_reporter = if ENV["SPEC_REPORTER"]
Minitest::Reporters::SpecReporter.new(color: true)
else
Minitest::Reporters::DefaultReporter.new(color: true)
end
Minitest::Reporters.use!(minitest_reporter)
# TODO: figure out how to re-enable (enabling it breaks minitest_test_runner_test)
# minitest_reporter = if ENV["SPEC_REPORTER"]
# Minitest::Reporters::SpecReporter.new(color: true)
# else
# Minitest::Reporters::DefaultReporter.new(color: true)
# end
# Minitest::Reporters.use!(minitest_reporter)

module Minitest
class Test
Expand Down

0 comments on commit 3295ec2

Please sign in to comment.