Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ jobs:
- rspec integration minitest
- rspec integration misc
- rspec integration rspec
- rspec integration tldr
- rspec unit
- rubocop
include:
Expand Down
11 changes: 11 additions & 0 deletions manager/src/ruby.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,11 @@ pub mod rspec {
/// Additional arguments
arguments: Vec<String>,
},
/// Run tldr integration specs
Tldr {
/// Additional arguments
arguments: Vec<String>,
},
/// Run rspec integration specs
Rspec {
/// Additional arguments
Expand Down Expand Up @@ -524,6 +529,12 @@ impl Runtime {
&arguments,
))
}
Some(rspec::integration::Command::Tldr { arguments }) => {
CommandConfig::ruby(bundle_exec_arguments(
&["rspec", "spec/integration", "-e", "tldr"],
&arguments,
))
}
Some(rspec::integration::Command::Rspec { arguments }) => {
CommandConfig::ruby(bundle_exec_arguments(
&["rspec", "spec/integration", "-e", "rspec"],
Expand Down
1 change: 1 addition & 0 deletions ruby/.rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,5 @@ Style/EndlessMethod:
Style/OneClassPerFile:
Exclude:
- 'lib/mutant/integration/minitest.rb'
- 'lib/mutant/integration/tldr.rb'
- 'spec/spec_helper.rb'
197 changes: 197 additions & 0 deletions ruby/lib/mutant/integration/tldr.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# frozen_string_literal: true

require 'mutant'
require 'tldr'
require 'mutant/tldr/coverage'

class TLDR
module Run
# Prevent autorun from running tests when the VM closes.
#
# Mutant needs control about the exit status of the VM and the moment of test
# execution.
#
# @api private
#
# @return [nil]
def self.at_exit!(*); end
end # Run

# Defang argv parsing — `tldr/autorun` evaluates `ArgvParser.new.parse(ARGV)`
# eagerly before passing the result to the no-op'd `at_exit!`, which would
# raise on mutant's own CLI flags.
class ArgvParser
def parse(*); end
end # ArgvParser
end # TLDR

module Mutant
class Integration
# tldr integration
class Tldr < self
TEST_FILE_PATTERN = './test/**/{test_*,*_test}.rb'
IDENTIFICATION_FORMAT = 'tldr:%s#%s'
CONFIG_OPTIONS = {
cli_defaults: false,
config_path: nil,
emoji: false,
fail_fast: false,
helper_paths: [],
load_paths: [],
no_helper: true,
no_prepend: true,
parallel: false,
prepend_paths: [],
reporter: 'Mutant::Integration::Tldr::Reporter',
timeout: -1,
warnings: false
}.freeze

# Quiet tldr reporter
class Reporter
def after_test(*) = nil
end # Reporter

# Compose a test class with one of its test methods
class TestCase
include Adamantium, Anima.new(:tldr_test)

# Identification string
#
# @return [String]
def identification = IDENTIFICATION_FORMAT % [klass, test_method]
memoize :identification

# Run test case
#
# TLDR::Runner terminates via Kernel.exit after every run. Kernel.exit
# raises SystemExit, so we can safely catch that here and convert the
# runner status into mutant's boolean result. Timeouts and fail-fast are
# disabled in the config, avoiding TLDR's hard-exit paths.
#
# @param [TLDR::Config] config
# @param [TLDR::Strategizer::Strategy] strategy
#
# @return [Boolean]
def call(config, strategy)
runner = ::TLDR::Runner.new
runner.run(config, ::TLDR::Plan.new([tldr_test], strategy))
rescue SystemExit => exception
exception.status.zero?
rescue StandardError
false
end

# Parse expressions
#
# @param [ExpressionParser] parser
#
# @return [Array<Expression>]
def expressions(parser)
klass.resolve_cover_expressions.to_a.map do |value|
parser.call(expand_constant(value)).from_right
end
end

def klass = tldr_test.test_class

def test_method = tldr_test.method_name.to_sym

private

def expand_constant(value)
case value
when Class, Module
"#{value.name}*"
else
value
end
end
end # TestCase

private_constant(*constants(false))

# Setup integration
#
# @return [self]
def setup
plan

self
end

# Call test integration
#
# @param [Array<Test>] tests
#
# @return [Result::Test]
def call(tests)
test_cases = tests.map(&all_tests_index.public_method(:fetch))
start = timer.now

passed = test_cases.all? { |test_case| test_case.call(config, sequential_strategy) }

Result::Test.new(
job_index: nil,
output: LogCapture::String.new(content: ''),
passed:,
runtime: timer.now - start
)
end

# All tests exposed by this integration
#
# @return [Array<Test>]
def all_tests = all_tests_index.keys
memoize :all_tests

alias_method :available_tests, :all_tests

private

def all_tests_index
all_test_cases.to_h do |test_case|
[construct_test(test_case), test_case]
end
end
memoize :all_tests_index

def construct_test(test_case)
Test.new(
id: test_case.identification,
expressions: test_case.expressions(expression_parser)
)
end

def all_test_cases
plan.tests.map do |test|
TestCase.new(tldr_test: test)
end
end

def test_paths
Pathname.glob(TEST_FILE_PATTERN).map(&:to_s)
end
memoize :test_paths

def config
::TLDR::Config.new(
**CONFIG_OPTIONS,
paths: test_paths,
seed: world.random.srand
).freeze
end
memoize :config

def plan
::TLDR::Planner.new.plan(config)
end
memoize :plan

def sequential_strategy
::TLDR::Strategizer::Strategy.new(parallel?: false)
end
memoize :sequential_strategy
end # Tldr
end # Integration
end # Mutant
51 changes: 51 additions & 0 deletions ruby/lib/mutant/tldr/coverage.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

require 'tldr'

module Mutant
module Tldr
module Coverage
# Setup coverage declaration for current class
#
# @param [String]
#
# @example
#
# class MyTest < TLDR
# cover 'MyCode*'
#
# def test_some_stuff
# end
# end
#
# @api public
def cover(expression)
@cover_expressions = Set.new unless defined?(@cover_expressions)

@cover_expressions << expression
end

# Effective coverage expression
#
# @return [Set<String>]
#
# @api private
def resolve_cover_expressions
return @cover_expressions if defined?(@cover_expressions)

try_superclass_cover_expressions
end

private

def try_superclass_cover_expressions
return unless superclass.respond_to?(:resolve_cover_expressions)

superclass.resolve_cover_expressions
end

end # Coverage
end # Tldr
end # Mutant

TLDR.extend(Mutant::Tldr::Coverage)
26 changes: 26 additions & 0 deletions ruby/mutant-tldr.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

require_relative 'lib/mutant/version'

Gem::Specification.new do |gem|
gem.name = 'mutant-tldr'
gem.version = Mutant::VERSION.dup
gem.authors = ['Markus Schirp']
gem.email = %w[mbj@schirp-dso.com]
gem.description = 'tldr integration for mutant'
gem.summary = gem.description
gem.homepage = 'https://github.com/mbj/mutant'
gem.license = 'Nonstandard'

gem.require_paths = %w[lib]
gem.files = %w[lib/mutant/tldr/coverage.rb lib/mutant/integration/tldr.rb]

gem.extra_rdoc_files = %w[LICENSE]

gem.required_ruby_version = '>= 3.3'

gem.metadata['rubygems_mfa_required'] = 'true'

gem.add_dependency('mutant', "= #{gem.version}")
gem.add_dependency('tldr', '~> 1.0')
end
2 changes: 1 addition & 1 deletion ruby/mutant.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Gem::Specification.new do |gem|

gem.require_paths = %w[lib]

exclusion = Dir.glob('lib/mutant/{integration/{minitest,rspec}.rb,minitest/**.rb}')
exclusion = Dir.glob('lib/mutant/{integration/{minitest,rspec,tldr}.rb,{minitest,tldr}/**.rb}')

gem.files = Dir.glob('{VERSION,lib/**/*}') - exclusion
gem.extra_rdoc_files = %w[LICENSE]
Expand Down
6 changes: 3 additions & 3 deletions ruby/spec/integration/mutant/minitest_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
%w[
bundle exec mutant run
--include test
--include lib
--include ../lib
--require test_app
--integration minitest
--usage opensource
]
end

let(:gemfile) { 'minitest/Gemfile' }
let(:gemfile) { 'Gemfile' }

it_behaves_like 'framework integration'
it_behaves_like 'framework integration', :minitest
end
6 changes: 3 additions & 3 deletions ruby/spec/integration/mutant/rspec_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
let(:base_cmd) do
%w[
bundle exec mutant run
--include lib
--include ../lib
--integration rspec
--require test_app
--usage opensource
Expand All @@ -13,9 +13,9 @@

%w[3.8 3.9 3.10 3.11 3.12 3.13 4.0].each do |version|
context "RSpec #{version}" do
let(:gemfile) { "rspec#{version}/Gemfile" }
let(:gemfile) { "Gemfile.rspec_#{version.tr('.', '_')}" }

it_behaves_like 'framework integration'
it_behaves_like 'framework integration', :rspec

it 'handles invalid rspec' do
Dir.chdir('test_app') do
Expand Down
18 changes: 18 additions & 0 deletions ruby/spec/integration/mutant/tldr_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

RSpec.describe 'tldr integration', mutant: false do
let(:base_cmd) do
%w[
bundle exec mutant run
--include test
--include ../lib
--require test_app
--integration tldr
--usage opensource
]
end

let(:gemfile) { 'Gemfile' }

it_behaves_like 'framework integration', :tldr
end
4 changes: 2 additions & 2 deletions ruby/spec/shared/framework_integration_behavior.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

RSpec.shared_examples_for 'framework integration' do
RSpec.shared_examples_for 'framework integration' do |integration_name|
def system_with_gemfile(*command)
Kernel.system(
{
Expand All @@ -14,7 +14,7 @@ def system_with_gemfile(*command)

around do |example|
Bundler.with_unbundled_env do
Dir.chdir(TestApp.root) do
Dir.chdir(File.join(TestApp.root, integration_name.to_s)) do
Kernel.system(
{ 'BUNDLE_PATH' => 'vendor/bundle' },
'bundle', 'install', '--gemfile', gemfile
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading