Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .circleci/gemspecs/compatible
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ Gem::Specification.new do |spec|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
spec.require_paths = %w[lib]

spec.add_runtime_dependency 'colorize', '>= 0.8.1'
spec.add_runtime_dependency 'rspec-core', '~> 3.10'
spec.add_runtime_dependency 'rspec-mocks', '~> 3.10'
spec.add_runtime_dependency 'terminal-table', '~> 3.0'

spec.add_development_dependency 'rspec', '~> 3.13'
end
2 changes: 2 additions & 0 deletions .circleci/gemspecs/latest
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ Gem::Specification.new do |spec|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
spec.require_paths = %w[lib]

spec.add_runtime_dependency 'colorize', '>= 0.8.1'
spec.add_runtime_dependency 'rspec-core', '~> 3.10'
spec.add_runtime_dependency 'rspec-mocks', '~> 3.10'
spec.add_runtime_dependency 'terminal-table', '~> 3.0'

spec.add_development_dependency 'bundler-audit', '~> 0.9.2'
spec.add_development_dependency 'fasterer', '~> 0.11.0'
Expand Down
1 change: 1 addition & 0 deletions .circleci/linter_configs/.commitspell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ languageSettings:
- GithubUser

words:
- Flexmock
- bagage
- bagages
- bestwebua
Expand Down
1 change: 1 addition & 0 deletions .circleci/linter_configs/.cspell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ languageSettings:

words:
- Commiting
- Flexmock
- Trotsenko
- Vladislav
- bestwebua
Expand Down
3 changes: 3 additions & 0 deletions .circleci/linter_configs/.fasterer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@

exclude_paths:
- '.circleci/**/*.rb'

speedups:
each_with_index_vs_while: false
7 changes: 7 additions & 0 deletions .reek.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ detectors:
exclude:
- RSpec::Mock::Context#respond_to_missing?

UtilityFunction:
exclude:
- ContextHelper#create_file

ManualDispatch:
exclude:
- RSpec::Mock::Context#method_missing
- RSpec::Mock::Context#respond_to_missing?

exclude_paths:
- lib/rspec/mock/migration_analytics/
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.0] - 2024-11-08

### Added

- Added CLI to analyze Flexmock usage and track migration progress to RSpec mocks.

## [0.2.0] - 2024-11-04

### Added
Expand Down
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- [Usage](#usage)
- [Configuration](#configuration)
- [Integration](#integration)
- [Migration Analytics](#migration-analytics)
- [Contributing](#contributing)
- [License](#license)
- [Code of Conduct](#code-of-conduct)
Expand Down Expand Up @@ -127,6 +128,46 @@ RSpec.describe Sandbox do
end
```

### Migration Analytics

You can create a Rake task to analyze Flexmock usage and track migration progress to RSpec mocks. Or use the CLI directly.

Example of the Rake task:

```ruby
namespace :rspec_mock do
namespace :migration_analytics do
desc 'Analyze Flexmock usage and track migration progress to RSpec mocks'
task :flexmock, %i[path] do |_, args|
require 'rspec/mock/migration_analytics/cli'

path = args[:path] || 'spec'
puts("\n🔍 Analyzing Flexmock usage in: #{path}")
RSpec::Mock::MigrationAnalytics::Cli.verify_path(path)
end
end
end
```

```bash
# Analyze entire spec directory (default)
rake rspec_mock:migration_analytics:flexmock

# Analyze specific directory
rake rspec_mock:migration_analytics:flexmock spec/services

# Analyze specific file
rake rspec_mock:migration_analytics:flexmock spec/services/sandbox_service_spec.rb
```

Example of the CLI usage:

```bash
ruby cli.rb spec
ruby cli.rb spec/services
ruby cli.rb spec/services/sandbox_service_spec.rb
```

## Contributing

Bug reports and pull requests are welcome on GitHub at <https://github.com/mocktools/ruby-rspec-mock>. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. Please check the [open tickets](https://github.com/mocktools/ruby-rspec-mock/issues). Be sure to follow Contributor Code of Conduct below and our [Contributing Guidelines](CONTRIBUTING.md).
Expand Down
11 changes: 11 additions & 0 deletions lib/rspec/mock/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@

module RSpec
module Mock
module MigrationAnalytics
module Tracker
require_relative 'migration_analytics/tracker/base'
require_relative 'migration_analytics/tracker/flexmock'
require_relative 'migration_analytics/tracker/rspec'
end

require_relative 'migration_analytics/file_analyzer'
require_relative 'migration_analytics/cli'
end

require_relative 'configuration'
require_relative 'context'
require_relative 'methods'
Expand Down
185 changes: 185 additions & 0 deletions lib/rspec/mock/migration_analytics/cli.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require 'colorize'
require 'terminal-table'

module RSpec
module Mock
module MigrationAnalytics
class Cli
class << self
def call
if ::ARGV.empty?
print_usage
exit 1
end

begin
verify_path(::ARGV[0])
rescue => error
puts("\n❌ Error: #{error.message}".red)
puts(error.backtrace) if ENV['DEBUG']
end
end

def verify_path(path)
case
when ::File.directory?(path) then verify_directory(path)
else verify_file(path)
end
end

private

def print_usage
puts('Usage: ruby cli.rb <path_to_spec_file_or_directory>'.yellow)
puts("\nExamples:".blue)
puts(' ruby cli.rb spec/models/user_spec.rb')
puts(' ruby cli.rb spec/models/')
puts(' ruby cli.rb spec/')
end

def verify_directory(dir_path) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
results = []
stats = {
total_files: 0,
files_with_mocks: 0,
total_flexmock_occurrences: 0,
total_rspec_mock_occurrences: 0,
files_with_mixed_usage: 0
}

::Dir.glob("#{dir_path}/**/*_spec.rb").each do |file|
stats[:total_files] += 1
result = RSpec::Mock::MigrationAnalytics::FileAnalyzer.call(file)

next unless result[:has_mocks]
stats[:files_with_mocks] += 1
stats[:total_flexmock_occurrences] += result[:flexmock_count]
stats[:total_rspec_mock_occurrences] += result[:rspec_mock_count]
stats[:files_with_mixed_usage] += 1 if result[:has_mixed_usage]
results << result
end

print_summary(results, stats)
end

def verify_file(file_path)
return puts("File not found: #{file_path}".red) unless ::File.exist?(file_path)
return puts("Not a Ruby spec file: #{file_path}".yellow) unless file_path.end_with?('_spec.rb')

print_file_result(RSpec::Mock::MigrationAnalytics::FileAnalyzer.call(file_path))
end

def print_file_result(result)
puts("\n=== Mock Usage Analysis: #{result[:file_path]} ===".blue)

if result[:has_mocks]
print_mock_statistics(result)
print_locations_table('Flexmock Usage', result[:flexmock_locations]) if result[:flexmock_locations].any?
print_locations_table('RSpec Mock Usage', result[:rspec_mock_locations]) if result[:rspec_mock_locations].any?
else
puts('✅ No mocking usage found'.green)
end
end

def print_summary(results, stats)
puts("\n=== Migration Status Report ===".blue)

total_mocks = stats[:total_flexmock_occurrences] + stats[:total_rspec_mock_occurrences]
migration_progress =
total_mocks.zero? ? 100 : (stats[:total_rspec_mock_occurrences].to_f / total_mocks * 100).round(2)

print_summary_table(stats, migration_progress)
print_files_table(results) if results.any?
end

def print_mock_statistics(result)
total_mocks = result[:flexmock_count] + result[:rspec_mock_count]
migration_progress = (result[:rspec_mock_count].to_f / total_mocks * 100).round(2)
puts(
Terminal::Table.new do |t|
t.add_row(['Total Mocks', total_mocks])
t.add_row(['Flexmock Usage', result[:flexmock_count]])
t.add_row(['RSpec Mock Usage', result[:rspec_mock_count]])
t.add_row(['Migration Progress', "#{migration_progress}%"])
end
)
end

def print_locations_table(title, locations)
return if locations.empty?

puts("\n#{title}:".yellow)
puts(
Terminal::Table.new do |table|
table.headings = %w[Line Type Content]
locations.each do |loc|
table.add_row(create_location_row(loc))
end
end
)
end

def create_location_row(loc)
type_str = loc[:type].nil? ? 'unknown' : loc[:type]
color = determine_color(loc[:type])

[
loc[:line_number].to_s.yellow,
type_str.respond_to?(color) ? type_str.send(color) : type_str,
loc[:content]
]
end

def determine_color(type)
case type
when 'migration mock block' then :cyan
when 'expect mock', 'allow mock' then :blue
when 'verifying double' then :green
else :light_white
end
end

def print_summary_table(stats, migration_progress)
puts(
Terminal::Table.new do |table|
table.add_row(['Total Spec Files', stats[:total_files]])
table.add_row(['Files with Mocks', stats[:files_with_mocks]])
table.add_row(['Files with Mixed Usage', stats[:files_with_mixed_usage]])
table.add_row(['Total Flexmock Occurrences', stats[:total_flexmock_occurrences]])
table.add_row(['Total RSpec Mock Occurrences', stats[:total_rspec_mock_occurrences]])
table.add_row(['Migration Progress', "#{migration_progress}%"])
end
)
end

def print_files_table(results)
puts("\n=== Files Requiring Migration ===".red)
puts(
Terminal::Table.new do |table|
table.headings = ['File Path', 'Flexmock Count', 'RSpec Mock Count', 'Progress']
results.sort_by { |row| -row[:flexmock_count] }.each do |result|
table.add_row(create_file_row(result))
end
end
)
end

def create_file_row(result)
total = result[:flexmock_count] + result[:rspec_mock_count]
progress =
total.zero? ? 100 : (result[:rspec_mock_count].to_f / total * 100).round(2)
[
result[:file_path],
result[:flexmock_count],
result[:rspec_mock_count],
"#{progress}%"
]
end
end
end
end
end
end
58 changes: 58 additions & 0 deletions lib/rspec/mock/migration_analytics/file_analyzer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

module RSpec
module Mock
module MigrationAnalytics
class FileAnalyzer
def self.call(
file_path,
flexmock_tracker = RSpec::Mock::MigrationAnalytics::Tracker::Flexmock.new,
rspec_tracker = RSpec::Mock::MigrationAnalytics::Tracker::Rspec.new
)
new(file_path, flexmock_tracker, rspec_tracker).call
end

def initialize(file_path, flexmock_tracker, rspec_tracker)
@file_path = file_path
@flexmock_tracker = flexmock_tracker
@rspec_tracker = rspec_tracker
end

def call
build_analytics
generate_report
end

private

attr_reader :file_path, :flexmock_tracker, :rspec_tracker

def build_analytics
::File.read(file_path).split("\n").each_with_index do |line, index|
line_number = index + 1
flexmock_tracker.scan_line(line, line_number)
rspec_tracker.scan_line(line, line_number)
end
end

%i[flexmock_tracker rspec_tracker].each do |method_name|
target_method_name = :"#{method_name}_locations"
define_method(target_method_name) { send(method_name).locations }
define_method(:"#{target_method_name}_any?") { send(target_method_name).any? }
end

def generate_report
{
file_path: file_path,
flexmock_count: flexmock_tracker_locations.size,
rspec_mock_count: rspec_tracker_locations.size,
flexmock_locations: flexmock_tracker_locations,
rspec_mock_locations: rspec_tracker_locations,
has_mocks: flexmock_tracker_locations_any? || rspec_tracker_locations_any?,
has_mixed_usage: flexmock_tracker_locations_any? && rspec_tracker_locations_any?
}
end
end
end
end
end
Loading