Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
Morriar committed Dec 13, 2024
1 parent d5ad5f2 commit 74981ad
Show file tree
Hide file tree
Showing 15 changed files with 703 additions and 0 deletions.
4 changes: 4 additions & 0 deletions lib/spoom.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
require "sorbet-runtime"
require "pathname"

require "coverage"
puts "#{Process.pid} starting coverage"
Coverage.start

module Spoom
extend T::Sig

Expand Down
4 changes: 4 additions & 0 deletions lib/spoom/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
require_relative "cli/helper"
require_relative "cli/deadcode"
require_relative "cli/srb"
require_relative "cli/tests"

module Spoom
module Cli
Expand Down Expand Up @@ -78,6 +79,9 @@ def lsp(*args)
invoke(Cli::Srb::LSP, args, options)
end

desc "tests", "Tests related commands"
subcommand "tests", Spoom::Cli::Tests

SORT_CODE = "code"
SORT_LOC = "loc"
SORT_ENUM = [SORT_CODE, SORT_LOC]
Expand Down
132 changes: 132 additions & 0 deletions lib/spoom/cli/tests.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# typed: true
# frozen_string_literal: true

require_relative "../tests"

module Spoom
module Cli
class Tests < Thor
include Helper

DEFAULT_OUTPUT_FILE = "coverage.json"

default_task :show

desc "show", "Show information about tests"
def show
guess_framework(context)
end

desc "list", "List tests"
def list
framework, test_files = guess_framework(context)

test_files.each do |test_file|
say(" * #{test_file.path}")
end

# TODO: match tests from args
end

# TODO: list test cases/suites + tests

desc "test", "Run tests"
def test(*paths)
context = self.context
framework, test_files = guess_framework(context)

if paths.any?
test_files = paths.flat_map { |path| context.glob(path) }.map { |path| Tests::File.new(path) }
end

say("\nRunning `#{test_files.size}` test files\n\n")

framework.install!(context)
framework.run_tests(context, test_files)
end

desc "coverage", "Run tests coverage"
option :output, type: :string, default: DEFAULT_OUTPUT_FILE, desc: "Output file"
def coverage(*paths)
context = self.context
framework, test_files = guess_framework(context)

if paths.any?
test_files = paths.flat_map { |path| context.glob(path) }.map { |path| Spoom::Tests::File.new(path) }
end

framework.install!(context)

coverage = framework.run_coverage(context, test_files)
compressed = []
coverage.results.each do |(test_case, test_coverage)|
compressed << {
test_case: test_case,
coverage: test_coverage.map do |file, lines|
[
file,
lines.map.with_index do |value, index|
next if value.nil? || value == 0

index + 1
end.compact,
]
end.select { |(_file, lines)| lines.any? }.compact.to_h,
}
end

output_file = Pathname.new(options[:output])
FileUtils.mkdir_p(output_file.dirname)
File.write(output_file, JSON.pretty_generate(compressed))
say("\nCoverage data saved to `#{output_file}`")
# TODO: tests
end

desc "map", "Map tests to source files"
option :output, type: :string, default: DEFAULT_OUTPUT_FILE, desc: "Output file"
def map(test_full_name)
hash = JSON.parse(File.read(options[:output]))

hash.each do |entry|
test_case = entry["test_case"]
next unless "#{test_case["klass"]}##{test_case["name"]}" == test_full_name

puts "#{test_case[:file]}:#{test_case[:line]}"

coverage = entry["coverage"]
coverage.each do |file, lines|
puts " #{file}"
lines.each_with_index do |line, index|
puts " #{index}: #{line}"
end
end
end
end

no_commands do
def guess_framework(context)
framework = begin
Spoom::Tests.guess_framework(context)
rescue Spoom::Tests::CantGuessTestFramework => e
say_error(e.message)
exit(1)
end

test_files = framework.test_files(context)
say("Matched framework `#{framework.framework_name}`, found `#{test_files.size}` test files")

[framework, test_files]
end
end
end
end
end

# spoom, rbi
# tapioca
# code-db
# core?

# TODO: tests
# TODO: run
# TODO: coverage
44 changes: 44 additions & 0 deletions lib/spoom/tests.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# typed: strict
# frozen_string_literal: true

require "coverage"

module Spoom
module Tests
class Error < Spoom::Error; end
end
end

require_relative "tests/file"
require_relative "tests/case"
require_relative "tests/coverage"
require_relative "tests/plugin"

module Spoom
module Tests
class CantGuessTestFramework < Error; end

class << self
extend T::Sig

sig { params(context: Context, try_frameworks: T::Array[T.class_of(Plugin)]).returns(T.class_of(Plugin)) }
def guess_framework(context, try_frameworks: DEFAULT_PLUGINS)
frameworks = try_frameworks.select { |plugin| plugin.match_context?(context) }

case frameworks.size
when 0
raise CantGuessTestFramework,
"No framework found for context. Tried #{try_frameworks.map(&:framework_name).join(", ")}"
when 1
return T.must(frameworks.first)
when 2
if frameworks.include?(Plugins::ActiveSupport) && frameworks.include?(Plugins::Minitest)
return Plugins::ActiveSupport
end
end

raise CantGuessTestFramework, "Multiple frameworks matching context: #{frameworks.map(&:name).join(", ")}"
end
end
end
end
44 changes: 44 additions & 0 deletions lib/spoom/tests/case.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# typed: strict
# frozen_string_literal: true

module Spoom
module Tests
class TestCase
extend T::Sig

sig { returns(String) }
attr_accessor :klass

sig { returns(String) }
attr_reader :name

sig { returns(String) }
attr_reader :file

sig { returns(Integer) }
attr_reader :line

sig { params(klass: String, name: String, file: String, line: Integer).void }
def initialize(klass:, name:, file:, line:)
@klass = klass
@name = name
@file = file
@line = line
end

sig { returns(String) }
def to_s
"#{klass}##{name} (#{file}:#{line})"
end

sig { params(args: T.untyped).returns(String) }
def to_json(*args)
T.unsafe({ klass:, name:, file:, line: }).to_json(*args)
end
end
end
end

# belongs to a test file
# has a name
# has associated files/lines (mapping)
28 changes: 28 additions & 0 deletions lib/spoom/tests/coverage.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# typed: strict
# frozen_string_literal: true

module Spoom
module Tests
class Coverage
extend T::Sig

attr_accessor :results

def initialize
@results = T.let([], T::Array[[TestCase, T::Hash[String, T::Array[T.nilable(Integer)]]]])
end

def <<((test_case, coverage))
@results << [test_case, coverage]
end

def to_json
results = []
@results.each do |test_case, coverage|
results << { test_case: test_case, coverage: coverage }
end
results.to_json
end
end
end
end
21 changes: 21 additions & 0 deletions lib/spoom/tests/file.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# typed: strict
# frozen_string_literal: true

module Spoom
module Tests
class File
extend T::Sig

sig { returns(String) }
attr_accessor :path

# TODO: add test cases
# TODO: mapping?

sig { params(path: String).void }
def initialize(path)
@path = path
end
end
end
end
62 changes: 62 additions & 0 deletions lib/spoom/tests/plugin.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# typed: strict
# frozen_string_literal: true

module Spoom
module Tests
class Plugin
extend T::Sig

class << self
extend T::Sig
extend T::Helpers

abstract!

sig { returns(String) }
def framework_name
T.must(name&.split("::")&.last)
end

sig { abstract.params(context: Context).returns(T::Boolean) }
def match_context?(context); end

sig { abstract.params(context: Context).returns(T::Array[Tests::File]) }
def test_files(context); end

sig { abstract.params(context: Context).void }
def install!(context); end

sig { abstract.params(context: Context, test_files: T::Array[Tests::File]).returns(T::Boolean) }
def run_tests(context, test_files); end

sig do
abstract.params(context: Context, test_files: T::Array[Tests::File]).returns(Coverage)
end
def run_coverage(context, test_files); end
end

# def run_tests
# end

# def run_test
# end
end
end
end

require_relative "plugins/minitest"
require_relative "plugins/rspec"
require_relative "plugins/active_support"

module Spoom
module Tests
DEFAULT_PLUGINS = T.let(
[
Plugins::Minitest,
Plugins::RSpec,
Plugins::ActiveSupport,
],
T::Array[T.class_of(Plugin)],
)
end
end
Loading

0 comments on commit 74981ad

Please sign in to comment.