Skip to content

Commit ee42c03

Browse files
authored
Feature/RSpec Mock migration analytics (#7)
* Added Flexmock/RSpec tracers, tests * Added FileAnalyzer, tests * Added CLI, tests * Updated gem version, changelog
1 parent 68c01f1 commit ee42c03

22 files changed

+1234
-1
lines changed

.circleci/gemspecs/compatible

+2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ Gem::Specification.new do |spec|
1616
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
1717
spec.require_paths = %w[lib]
1818

19+
spec.add_runtime_dependency 'colorize', '>= 0.8.1'
1920
spec.add_runtime_dependency 'rspec-core', '~> 3.10'
2021
spec.add_runtime_dependency 'rspec-mocks', '~> 3.10'
22+
spec.add_runtime_dependency 'terminal-table', '~> 3.0'
2123

2224
spec.add_development_dependency 'rspec', '~> 3.13'
2325
end

.circleci/gemspecs/latest

+2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ Gem::Specification.new do |spec|
1616
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
1717
spec.require_paths = %w[lib]
1818

19+
spec.add_runtime_dependency 'colorize', '>= 0.8.1'
1920
spec.add_runtime_dependency 'rspec-core', '~> 3.10'
2021
spec.add_runtime_dependency 'rspec-mocks', '~> 3.10'
22+
spec.add_runtime_dependency 'terminal-table', '~> 3.0'
2123

2224
spec.add_development_dependency 'bundler-audit', '~> 0.9.2'
2325
spec.add_development_dependency 'fasterer', '~> 0.11.0'

.circleci/linter_configs/.commitspell.yml

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ languageSettings:
1313
- GithubUser
1414

1515
words:
16+
- Flexmock
1617
- bagage
1718
- bagages
1819
- bestwebua

.circleci/linter_configs/.cspell.yml

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ languageSettings:
2020

2121
words:
2222
- Commiting
23+
- Flexmock
2324
- Trotsenko
2425
- Vladislav
2526
- bestwebua

.circleci/linter_configs/.fasterer.yml

+3
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22

33
exclude_paths:
44
- '.circleci/**/*.rb'
5+
6+
speedups:
7+
each_with_index_vs_while: false

.reek.yml

+7
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@ detectors:
88
exclude:
99
- RSpec::Mock::Context#respond_to_missing?
1010

11+
UtilityFunction:
12+
exclude:
13+
- ContextHelper#create_file
14+
1115
ManualDispatch:
1216
exclude:
1317
- RSpec::Mock::Context#method_missing
1418
- RSpec::Mock::Context#respond_to_missing?
19+
20+
exclude_paths:
21+
- lib/rspec/mock/migration_analytics/

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
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).
44

5+
## [0.3.0] - 2024-11-08
6+
7+
### Added
8+
9+
- Added CLI to analyze Flexmock usage and track migration progress to RSpec mocks.
10+
511
## [0.2.0] - 2024-11-04
612

713
### Added

README.md

+41
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
- [Usage](#usage)
1919
- [Configuration](#configuration)
2020
- [Integration](#integration)
21+
- [Migration Analytics](#migration-analytics)
2122
- [Contributing](#contributing)
2223
- [License](#license)
2324
- [Code of Conduct](#code-of-conduct)
@@ -127,6 +128,46 @@ RSpec.describe Sandbox do
127128
end
128129
```
129130

131+
### Migration Analytics
132+
133+
You can create a Rake task to analyze Flexmock usage and track migration progress to RSpec mocks. Or use the CLI directly.
134+
135+
Example of the Rake task:
136+
137+
```ruby
138+
namespace :rspec_mock do
139+
namespace :migration_analytics do
140+
desc 'Analyze Flexmock usage and track migration progress to RSpec mocks'
141+
task :flexmock, %i[path] do |_, args|
142+
require 'rspec/mock/migration_analytics/cli'
143+
144+
path = args[:path] || 'spec'
145+
puts("\n🔍 Analyzing Flexmock usage in: #{path}")
146+
RSpec::Mock::MigrationAnalytics::Cli.verify_path(path)
147+
end
148+
end
149+
end
150+
```
151+
152+
```bash
153+
# Analyze entire spec directory (default)
154+
rake rspec_mock:migration_analytics:flexmock
155+
156+
# Analyze specific directory
157+
rake rspec_mock:migration_analytics:flexmock spec/services
158+
159+
# Analyze specific file
160+
rake rspec_mock:migration_analytics:flexmock spec/services/sandbox_service_spec.rb
161+
```
162+
163+
Example of the CLI usage:
164+
165+
```bash
166+
ruby cli.rb spec
167+
ruby cli.rb spec/services
168+
ruby cli.rb spec/services/sandbox_service_spec.rb
169+
```
170+
130171
## Contributing
131172

132173
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).

lib/rspec/mock/core.rb

+11
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@
55

66
module RSpec
77
module Mock
8+
module MigrationAnalytics
9+
module Tracker
10+
require_relative 'migration_analytics/tracker/base'
11+
require_relative 'migration_analytics/tracker/flexmock'
12+
require_relative 'migration_analytics/tracker/rspec'
13+
end
14+
15+
require_relative 'migration_analytics/file_analyzer'
16+
require_relative 'migration_analytics/cli'
17+
end
18+
819
require_relative 'configuration'
920
require_relative 'context'
1021
require_relative 'methods'
+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require 'colorize'
5+
require 'terminal-table'
6+
7+
module RSpec
8+
module Mock
9+
module MigrationAnalytics
10+
class Cli
11+
class << self
12+
def call
13+
if ::ARGV.empty?
14+
print_usage
15+
exit 1
16+
end
17+
18+
begin
19+
verify_path(::ARGV[0])
20+
rescue => error
21+
puts("\n❌ Error: #{error.message}".red)
22+
puts(error.backtrace) if ENV['DEBUG']
23+
end
24+
end
25+
26+
def verify_path(path)
27+
case
28+
when ::File.directory?(path) then verify_directory(path)
29+
else verify_file(path)
30+
end
31+
end
32+
33+
private
34+
35+
def print_usage
36+
puts('Usage: ruby cli.rb <path_to_spec_file_or_directory>'.yellow)
37+
puts("\nExamples:".blue)
38+
puts(' ruby cli.rb spec/models/user_spec.rb')
39+
puts(' ruby cli.rb spec/models/')
40+
puts(' ruby cli.rb spec/')
41+
end
42+
43+
def verify_directory(dir_path) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
44+
results = []
45+
stats = {
46+
total_files: 0,
47+
files_with_mocks: 0,
48+
total_flexmock_occurrences: 0,
49+
total_rspec_mock_occurrences: 0,
50+
files_with_mixed_usage: 0
51+
}
52+
53+
::Dir.glob("#{dir_path}/**/*_spec.rb").each do |file|
54+
stats[:total_files] += 1
55+
result = RSpec::Mock::MigrationAnalytics::FileAnalyzer.call(file)
56+
57+
next unless result[:has_mocks]
58+
stats[:files_with_mocks] += 1
59+
stats[:total_flexmock_occurrences] += result[:flexmock_count]
60+
stats[:total_rspec_mock_occurrences] += result[:rspec_mock_count]
61+
stats[:files_with_mixed_usage] += 1 if result[:has_mixed_usage]
62+
results << result
63+
end
64+
65+
print_summary(results, stats)
66+
end
67+
68+
def verify_file(file_path)
69+
return puts("File not found: #{file_path}".red) unless ::File.exist?(file_path)
70+
return puts("Not a Ruby spec file: #{file_path}".yellow) unless file_path.end_with?('_spec.rb')
71+
72+
print_file_result(RSpec::Mock::MigrationAnalytics::FileAnalyzer.call(file_path))
73+
end
74+
75+
def print_file_result(result)
76+
puts("\n=== Mock Usage Analysis: #{result[:file_path]} ===".blue)
77+
78+
if result[:has_mocks]
79+
print_mock_statistics(result)
80+
print_locations_table('Flexmock Usage', result[:flexmock_locations]) if result[:flexmock_locations].any?
81+
print_locations_table('RSpec Mock Usage', result[:rspec_mock_locations]) if result[:rspec_mock_locations].any?
82+
else
83+
puts('✅ No mocking usage found'.green)
84+
end
85+
end
86+
87+
def print_summary(results, stats)
88+
puts("\n=== Migration Status Report ===".blue)
89+
90+
total_mocks = stats[:total_flexmock_occurrences] + stats[:total_rspec_mock_occurrences]
91+
migration_progress =
92+
total_mocks.zero? ? 100 : (stats[:total_rspec_mock_occurrences].to_f / total_mocks * 100).round(2)
93+
94+
print_summary_table(stats, migration_progress)
95+
print_files_table(results) if results.any?
96+
end
97+
98+
def print_mock_statistics(result)
99+
total_mocks = result[:flexmock_count] + result[:rspec_mock_count]
100+
migration_progress = (result[:rspec_mock_count].to_f / total_mocks * 100).round(2)
101+
puts(
102+
Terminal::Table.new do |t|
103+
t.add_row(['Total Mocks', total_mocks])
104+
t.add_row(['Flexmock Usage', result[:flexmock_count]])
105+
t.add_row(['RSpec Mock Usage', result[:rspec_mock_count]])
106+
t.add_row(['Migration Progress', "#{migration_progress}%"])
107+
end
108+
)
109+
end
110+
111+
def print_locations_table(title, locations)
112+
return if locations.empty?
113+
114+
puts("\n#{title}:".yellow)
115+
puts(
116+
Terminal::Table.new do |table|
117+
table.headings = %w[Line Type Content]
118+
locations.each do |loc|
119+
table.add_row(create_location_row(loc))
120+
end
121+
end
122+
)
123+
end
124+
125+
def create_location_row(loc)
126+
type_str = loc[:type].nil? ? 'unknown' : loc[:type]
127+
color = determine_color(loc[:type])
128+
129+
[
130+
loc[:line_number].to_s.yellow,
131+
type_str.respond_to?(color) ? type_str.send(color) : type_str,
132+
loc[:content]
133+
]
134+
end
135+
136+
def determine_color(type)
137+
case type
138+
when 'migration mock block' then :cyan
139+
when 'expect mock', 'allow mock' then :blue
140+
when 'verifying double' then :green
141+
else :light_white
142+
end
143+
end
144+
145+
def print_summary_table(stats, migration_progress)
146+
puts(
147+
Terminal::Table.new do |table|
148+
table.add_row(['Total Spec Files', stats[:total_files]])
149+
table.add_row(['Files with Mocks', stats[:files_with_mocks]])
150+
table.add_row(['Files with Mixed Usage', stats[:files_with_mixed_usage]])
151+
table.add_row(['Total Flexmock Occurrences', stats[:total_flexmock_occurrences]])
152+
table.add_row(['Total RSpec Mock Occurrences', stats[:total_rspec_mock_occurrences]])
153+
table.add_row(['Migration Progress', "#{migration_progress}%"])
154+
end
155+
)
156+
end
157+
158+
def print_files_table(results)
159+
puts("\n=== Files Requiring Migration ===".red)
160+
puts(
161+
Terminal::Table.new do |table|
162+
table.headings = ['File Path', 'Flexmock Count', 'RSpec Mock Count', 'Progress']
163+
results.sort_by { |row| -row[:flexmock_count] }.each do |result|
164+
table.add_row(create_file_row(result))
165+
end
166+
end
167+
)
168+
end
169+
170+
def create_file_row(result)
171+
total = result[:flexmock_count] + result[:rspec_mock_count]
172+
progress =
173+
total.zero? ? 100 : (result[:rspec_mock_count].to_f / total * 100).round(2)
174+
[
175+
result[:file_path],
176+
result[:flexmock_count],
177+
result[:rspec_mock_count],
178+
"#{progress}%"
179+
]
180+
end
181+
end
182+
end
183+
end
184+
end
185+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# frozen_string_literal: true
2+
3+
module RSpec
4+
module Mock
5+
module MigrationAnalytics
6+
class FileAnalyzer
7+
def self.call(
8+
file_path,
9+
flexmock_tracker = RSpec::Mock::MigrationAnalytics::Tracker::Flexmock.new,
10+
rspec_tracker = RSpec::Mock::MigrationAnalytics::Tracker::Rspec.new
11+
)
12+
new(file_path, flexmock_tracker, rspec_tracker).call
13+
end
14+
15+
def initialize(file_path, flexmock_tracker, rspec_tracker)
16+
@file_path = file_path
17+
@flexmock_tracker = flexmock_tracker
18+
@rspec_tracker = rspec_tracker
19+
end
20+
21+
def call
22+
build_analytics
23+
generate_report
24+
end
25+
26+
private
27+
28+
attr_reader :file_path, :flexmock_tracker, :rspec_tracker
29+
30+
def build_analytics
31+
::File.read(file_path).split("\n").each_with_index do |line, index|
32+
line_number = index + 1
33+
flexmock_tracker.scan_line(line, line_number)
34+
rspec_tracker.scan_line(line, line_number)
35+
end
36+
end
37+
38+
%i[flexmock_tracker rspec_tracker].each do |method_name|
39+
target_method_name = :"#{method_name}_locations"
40+
define_method(target_method_name) { send(method_name).locations }
41+
define_method(:"#{target_method_name}_any?") { send(target_method_name).any? }
42+
end
43+
44+
def generate_report
45+
{
46+
file_path: file_path,
47+
flexmock_count: flexmock_tracker_locations.size,
48+
rspec_mock_count: rspec_tracker_locations.size,
49+
flexmock_locations: flexmock_tracker_locations,
50+
rspec_mock_locations: rspec_tracker_locations,
51+
has_mocks: flexmock_tracker_locations_any? || rspec_tracker_locations_any?,
52+
has_mixed_usage: flexmock_tracker_locations_any? && rspec_tracker_locations_any?
53+
}
54+
end
55+
end
56+
end
57+
end
58+
end

0 commit comments

Comments
 (0)