Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# main ([unreleased](https://github.com/fastruby/rails_stats/compare/v2.1.0...main))

* [FEATURE: Support component-based and Packwerk (packs) applications with a per-pack and Core breakdown](https://github.com/fastruby/rails_stats/issues/23)
* [BUGFIX: Respect an explicit path argument (e.g. `rake stats[packs/pack1]`) when running inside a booted Rails app](https://github.com/fastruby/rails_stats/issues/23)
* [CHORE: Improve the GH Test Workflow](https://github.com/fastruby/rails_stats/pull/35)
* [BUGFIX: Explicitly set format as text for `Bundler::Stats::CLI` on `ConsoleFormatter`](https://github.com/fastruby/rails_stats/pull/43)

Expand Down
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,56 @@ Directory: /path/to/app/
$ for dir in /path/to/many/engines/*/; do bundle exec rake stats[$dir]; done
```

### Component-based and Packwerk (packs) applications

RailsStats understands large Rails monoliths that are organized into
[packs](https://github.com/Shopify/packwerk) (via
[packs-rails](https://github.com/rubyatscale/packs-rails)) or
[components](https://cbra.info/) (component-based Rails).

When packs or components are detected, `rake stats` automatically prints a
breakdown per pack/component, a `Core Application` section for everything that
lives in the main app, and a `Total (all packs)` section that aggregates the
whole monolith:

```bash
$ bundle exec rake stats

== Core Application ==
+----------------------+---------+---------+---------+---------+---------+-----+-------+
| Name | Files | Lines | LOC | Classes | Methods | M/C | LOC/M |
+----------------------+---------+---------+---------+---------+---------+-----+-------+
| ... |
+----------------------+---------+---------+---------+---------+---------+-----+-------+

== Pack: packs/pack1 ==
+----------------------+---------+---------+---------+---------+---------+-----+-------+
| ... |
+----------------------+---------+---------+---------+---------+---------+-----+-------+

== Total (all packs) ==
+----------------------+---------+---------+---------+---------+---------+-----+-------+
| ... |
+----------------------+---------+---------+---------+---------+---------+-----+-------+
```

A directory is treated as a pack when it contains a `package.yml` file (the
Packwerk marker) or when it lives directly under a `packs/` or `components/`
directory and holds any of the usual code folders (`app`, `lib`, `spec`,
`test`). Nested packs (`packs/a/packs/b`) are supported.

You can also scope stats to a single pack, even from inside a booted Rails app:

```bash
$ bundle exec rake stats[packs/pack1]
```

The same breakdown is available in JSON under a `"packs"` key:

```bash
$ bundle exec rake stats[.,json]
```

### Within your Rails project

You can also include it within your Rails application to _replace_ the default `rake stats` implementation.
Expand Down
1 change: 1 addition & 0 deletions lib/rails_stats/all.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require 'rails_stats/console_formatter'
require 'rails_stats/inflector'
require 'rails_stats/code_statistics_calculator'
require 'rails_stats/pack_finder'
require 'rails_stats/util'
require 'rails_stats/app_statistics'
require 'rails_stats/test_statistics'
Expand Down
2 changes: 1 addition & 1 deletion lib/rails_stats/app_statistics.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module RailsStats
class AppStatistics
attr_reader :statistics, :total, :test
attr_reader :statistics, :total, :test, :directory

def initialize(directory)
@directories = []
Expand Down
52 changes: 42 additions & 10 deletions lib/rails_stats/console_formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,10 @@ class ConsoleFormatter < StatsFormatter
def to_s
Bundler::Stats::CLI.start(['--format', 'text'])

print_header
sorted_keys = @statistics.keys.sort
sorted_keys.each { |key| print_line(key, @statistics[key]) }
print_splitter

if @grand_total
print_line("Code", @code_total)
print_line("Tests", @tests_total)
print_line("Total", @grand_total)
print_splitter
if calculator.packs?
print_grouped
else
print_flat
end

print_code_test_stats
Expand All @@ -24,6 +18,44 @@ def to_s

private

def print_flat
print_table(@statistics)
end

def print_grouped
@statistics_by_group.each do |group, group_statistics|
puts "== #{group_label(group)} =="
print_table(group_statistics)
puts ""
end

puts "== #{grand_total_label} =="
print_table(@statistics)
end

def group_label(group)
group == RailsStats::StatsCalculator::CORE_GROUP ? "Core Application" : "Pack: #{group}"
end

def grand_total_label
"Total (all packs)"
end

# Prints a full table (header, one line per concept, and Code/Tests/Total
# totals) for the given {concept => CodeStatisticsCalculator} hash.
def print_table(statistics)
code, tests, grand = calculator.totals_for(statistics)

print_header
statistics.keys.sort.each { |key| print_line(key, statistics[key]) }
print_splitter

print_line("Code", code)
print_line("Tests", tests)
print_line("Total", grand)
print_splitter
end

def print_header
print_splitter
puts "| Name | Files | Lines | LOC | Classes | Methods | M/C | LOC/M |"
Expand Down
2 changes: 1 addition & 1 deletion lib/rails_stats/cucumber_statistics.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module RailsStats
class CucumberStatistics
attr_reader :statistics, :total, :test
attr_reader :statistics, :total, :test, :directory

def initialize(directory)
@test = true
Expand Down
2 changes: 1 addition & 1 deletion lib/rails_stats/gem_statistics.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module RailsStats
class GemStatistics
attr_reader :statistics, :total, :test
attr_reader :statistics, :total, :test, :directory

def initialize(directory)
@test = false
Expand Down
18 changes: 18 additions & 0 deletions lib/rails_stats/json_formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ def result
@result << stat_hash("Tests", @tests_total).merge(code_test_hash) if @tests_total
@result << stat_hash("Total", @grand_total).merge(code_test_hash) if @grand_total

@result << { "packs" => packs_breakdown } if calculator.packs?

@result << { "schema_stats" => schema_info }
@result << { "polymorphic_stats" => print_polymorphic_stats }

Expand All @@ -29,6 +31,22 @@ def to_s

private

# Per-pack / per-component breakdown, keyed by group name ("Core" plus
# each pack's relative path). Each group lists its concept stats followed
# by Code/Tests/Total rows for that group.
def packs_breakdown
@statistics_by_group.each_with_object({}) do |(group, group_statistics), out|
code, tests, grand = calculator.totals_for(group_statistics)

rows = group_statistics.map { |key, stats| stat_hash(key, stats) }
rows << stat_hash("Code", code).merge("total" => true)
rows << stat_hash("Tests", tests).merge("total" => true)
rows << stat_hash("Total", grand).merge("total" => true)

out[group] = rows
end
end

def code_test_hash
code = calculator.code_loc
tests = calculator.test_loc
Expand Down
64 changes: 64 additions & 0 deletions lib/rails_stats/pack_finder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
module RailsStats
# Detects "packs" (packwerk / packs-rails) and "components" (component-based
# Rails, à la Stephan Hagemann) inside a Rails application so that stats can
# be reported per pack/component plus a "Core" group for everything else.
#
# A directory is considered a pack when either:
#
# * it contains a `package.yml` file (the packwerk marker), or
# * it is a direct child of a `packs/` or `components/` directory and holds
# any of the usual code folders (`app`, `lib`, `spec`, `test`).
#
# Nested packs (e.g. `packs/a/packs/b`) are supported. The root directory
# itself is never reported as a pack.
module PackFinder
extend self

CODE_FOLDERS = %w[app lib spec test].freeze
PACK_CONTAINERS = %w[packs components].freeze
IGNORED_PATH = %r{/(vendor|node_modules|tmp|\.git)/}.freeze

# Returns the absolute paths of every detected pack/component, sorted.
def find(root_directory)
root = File.absolute_path(root_directory)
packs = {}

collect_packwerk_packs(root, packs)
collect_convention_packs(root, packs)

packs.keys.sort
end

private

def collect_packwerk_packs(root, packs)
Dir.glob(File.join(root, "**", "package.yml")).each do |marker_path|
next if marker_path =~ IGNORED_PATH

dir = File.absolute_path(File.dirname(marker_path))
next if dir == root

packs[dir] = true
end
end

def collect_convention_packs(root, packs)
PACK_CONTAINERS.each do |container|
Dir.glob(File.join(root, "**", container, "*")).each do |path|
next if path =~ IGNORED_PATH
Comment on lines +38 to +51
next unless File.directory?(path)

dir = File.absolute_path(path)
next if dir == root
next unless code_folder?(dir)

packs[dir] = true
end
end
end

def code_folder?(dir)
CODE_FOLDERS.any? { |folder| File.directory?(File.join(dir, folder)) }
end
end
end
2 changes: 1 addition & 1 deletion lib/rails_stats/root_statistics.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module RailsStats
class RootStatistics
attr_reader :statistics, :total, :test
attr_reader :statistics, :total, :test, :directory

ROOT_FOLDERS = {
"lib" => "Libraries",
Expand Down
2 changes: 1 addition & 1 deletion lib/rails_stats/spec_statistics.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module RailsStats
class SpecStatistics
attr_reader :statistics, :total, :test
attr_reader :statistics, :total, :test, :directory

SPEC_FOLDERS = ['controllers',
'features',
Expand Down
Loading
Loading