rspec-candy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: eb539726790eb4bce8711473b8d0a427d1efb63628d1b481bb5e32c80ebc2d84
4
+ data.tar.gz: 82fe832e4fbfe9261fdc089da5ddd708d71757e866f8a0d3daffabbfca0786d2
5
+ SHA512:
6
+ metadata.gz: b48d2a468638db3c12ac89f5f5dd0d26ef2197e71cc65f64372e5e2663af85c93f8e5e47983714971d4b7ce2df9a75e4f6b48ba599cecd11c7d51c40b1dd530d
7
+ data.tar.gz: 92222aef2a3a63af58517433e938f213117c45faff38b69c202040e94c32884507fd7f8e1084feb920267c5c69c8168e2abb32804c0055eadfb812f128d0a0b5
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Adam Ladachowski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # RSpec::Candy
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/rspec-candy.svg)](https://badge.fury.io/rb/rspec-candy)
4
+ [![CI](https://github.com/aladac/rspec-candy/actions/workflows/main.yml/badge.svg)](https://github.com/aladac/rspec-candy/actions/workflows/main.yml)
5
+
6
+ Eye candy for RSpec: beautiful progress bars and coverage reports.
7
+
8
+ ## Features
9
+
10
+ - **TTY Progress Bar** - Rich progress bar with ETA, rate, and percentage
11
+ - **Colored Output** - Green for pass, red for fail, yellow for pending
12
+ - **Coverage Table** - Automatically displays if SimpleCov data exists
13
+ - **Non-TTY Fallback** - Dots/characters when not in a terminal
14
+
15
+ ## Installation
16
+
17
+ Add to your Gemfile:
18
+
19
+ ```ruby
20
+ group :test do
21
+ gem "rspec-candy"
22
+ end
23
+ ```
24
+
25
+ Then:
26
+
27
+ ```bash
28
+ bundle install
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ Add to `.rspec`:
34
+
35
+ ```
36
+ --require rspec/candy
37
+ --format RSpec::Candy::Formatter
38
+ ```
39
+
40
+ Then just run:
41
+
42
+ ```bash
43
+ bundle exec rspec
44
+ ```
45
+
46
+ You get a progress bar, test results, and coverage table (if SimpleCov is configured) - all in one.
47
+
48
+ ## Screenshot
49
+
50
+ ```
51
+ Running 42 examples...
52
+
53
+ [██████████████████████████████████████░░░░░░░░░░] 38/42 90% │ 00:01
54
+
55
+ ✓ 42 examples, 0 failures
56
+ Finished in 0.73s
57
+
58
+ ┌─────────────┬────────┬────────────────┬───────┐
59
+ │ File │ Lines │ Coverage │ % │
60
+ ├─────────────┼────────┼────────────────┼───────┤
61
+ │ cli.rb │ 85/100 │ █████████████░ │ 85.0% │
62
+ │ builder.rb │ 38/50 │ ███████████░░░ │ 76.0% │
63
+ │ remote.rb │ 22/40 │ ████████░░░░░░ │ 55.0% │
64
+ └─────────────┴────────┴────────────────┴───────┘
65
+
66
+ Total: 145/190 (76.3%)
67
+ 4 files at 100% (not shown)
68
+ ```
69
+
70
+ ## Requirements
71
+
72
+ - Ruby >= 3.2.0
73
+ - RSpec >= 3.0
74
+ - SimpleCov (for coverage reports)
75
+
76
+ ## License
77
+
78
+ MIT License. See [LICENSE.txt](LICENSE.txt).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[spec standard]
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "pastel"
5
+ require "tty-table"
6
+ require "tty-progressbar"
7
+
8
+ module RSpec
9
+ module Candy
10
+ class Coverage
11
+ DEFAULT_COVERAGE_PATH = "coverage/.resultset.json"
12
+
13
+ def initialize(coverage_path: nil, lib_filter: "lib/")
14
+ @coverage_path = coverage_path || DEFAULT_COVERAGE_PATH
15
+ @lib_filter = lib_filter
16
+ @pastel = Pastel.new
17
+ end
18
+
19
+ def report(silent: false)
20
+ unless File.exist?(@coverage_path)
21
+ return true if silent
22
+ warn @pastel.red("Coverage data not found at #{@coverage_path}")
23
+ warn @pastel.dim("Run your tests first to generate coverage data")
24
+ return false
25
+ end
26
+
27
+ data = JSON.parse(File.read(@coverage_path))
28
+ coverage = data.values.first["coverage"]
29
+
30
+ results = parse_results(coverage)
31
+ return true if results.empty?
32
+
33
+ print_table(results)
34
+ true
35
+ end
36
+
37
+ private
38
+
39
+ def parse_results(coverage)
40
+ coverage.filter_map do |file, cov_data|
41
+ next unless file.include?(@lib_filter)
42
+
43
+ lines = cov_data["lines"]
44
+ name = file.sub(%r{.*/#{Regexp.escape(@lib_filter)}}, @lib_filter)
45
+ total = lines.count { |l| l.is_a?(Integer) }
46
+ covered = lines.count { |l| l.is_a?(Integer) && l.positive? }
47
+ pct = total.positive? ? (covered.to_f / total * 100).round(1) : 100.0
48
+
49
+ [name, covered, total, pct]
50
+ end
51
+ end
52
+
53
+ def print_table(results)
54
+ full_coverage_count = results.count { |r| r[3] == 100.0 }
55
+ incomplete = results.reject { |r| r[3] == 100.0 }.sort_by { |r| -r[3] }
56
+ totals = results.reduce([0, 0]) { |acc, r| [acc[0] + r[1], acc[1] + r[2]] }
57
+ total_pct = (totals[0].to_f / totals[1] * 100).round(1)
58
+
59
+ rows = incomplete.map do |name, covered, total, pct|
60
+ short = name.sub(%r{^#{Regexp.escape(@lib_filter)}}, "")
61
+ bar = progress_bar(pct)
62
+ [short, "#{covered}/#{total}", bar, "#{pct}%"]
63
+ end
64
+
65
+ table = TTY::Table.new(
66
+ header: [@pastel.bold("File"), @pastel.bold("Lines"), @pastel.bold("Coverage"), @pastel.bold("%")],
67
+ rows: rows
68
+ )
69
+
70
+ puts
71
+ puts table.render(:unicode, padding: [0, 1]) { |r| r.border.style = :dim }
72
+ puts
73
+ puts @pastel.bold("Total: ") + "#{totals[0]}/#{totals[1]} (#{total_pct}%)"
74
+ puts @pastel.dim("#{full_coverage_count} files at 100% (not shown)")
75
+ end
76
+
77
+ def progress_bar(pct, width: 20)
78
+ filled = (pct / 100.0 * width).round
79
+ empty = width - filled
80
+ color = if pct >= 80
81
+ :green
82
+ elsif pct >= 50
83
+ :yellow
84
+ else
85
+ :red
86
+ end
87
+ @pastel.decorate("█" * filled, color) + @pastel.dim("░" * empty)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/core"
4
+ require "tty-progressbar"
5
+ require "pastel"
6
+
7
+ module RSpec
8
+ module Candy
9
+ class Formatter
10
+ ::RSpec::Core::Formatters.register self,
11
+ :start, :example_passed, :example_failed, :example_pending, :stop, :dump_summary
12
+
13
+ def initialize(output)
14
+ @output = output
15
+ @failed = []
16
+ @pending = []
17
+ @pastel = Pastel.new(enabled: output.tty?)
18
+ @tty = output.tty?
19
+ end
20
+
21
+ def start(notification)
22
+ @total = notification.count
23
+ @current = 0
24
+ @output.puts @pastel.cyan("Running #{@total} examples...")
25
+ @output.puts
26
+ return unless @tty
27
+
28
+ require "tty-screen"
29
+ term_width = TTY::Screen.width rescue 140
30
+ bar_width = [term_width - 45, 80].min
31
+ @bar = TTY::ProgressBar.new(
32
+ "[:bar] :current/:total :percent │ :eta_time │ :rate/s",
33
+ total: @total,
34
+ width: bar_width,
35
+ complete: @pastel.green("█"),
36
+ incomplete: @pastel.dim("░"),
37
+ output: @output,
38
+ hide_cursor: true,
39
+ frequency: 10
40
+ )
41
+ @bar.start
42
+ end
43
+
44
+ def example_passed(_notification)
45
+ @current += 1
46
+ @tty ? @bar.advance : @output.print(@pastel.green("."))
47
+ end
48
+
49
+ def example_failed(notification)
50
+ @current += 1
51
+ @failed << notification
52
+ @tty ? @bar.advance : @output.print(@pastel.red("F"))
53
+ end
54
+
55
+ def example_pending(notification)
56
+ @current += 1
57
+ @pending << notification
58
+ @tty ? @bar.advance : @output.print(@pastel.yellow("*"))
59
+ end
60
+
61
+ def stop(_notification)
62
+ @bar&.finish
63
+ @output.puts
64
+ end
65
+
66
+ def dump_summary(summary)
67
+ @output.puts
68
+ print_failures if @failed.any?
69
+
70
+ status = if @failed.any?
71
+ @pastel.red.bold("✗ #{summary.example_count} examples, #{@failed.size} failures")
72
+ else
73
+ @pastel.green.bold("✓ #{summary.example_count} examples, 0 failures")
74
+ end
75
+ status += @pastel.yellow(" (#{@pending.size} pending)") if @pending.any?
76
+ @output.puts status
77
+ @output.puts @pastel.dim("Finished in #{summary.duration.round(2)}s")
78
+
79
+ print_coverage if coverage_available?
80
+ end
81
+
82
+ private
83
+
84
+ def coverage_available?
85
+ File.exist?(Coverage::DEFAULT_COVERAGE_PATH)
86
+ end
87
+
88
+ def print_coverage
89
+ Coverage.new.report(silent: true)
90
+ end
91
+
92
+ def print_failures
93
+ @output.puts @pastel.red.bold("\nFailures:\n")
94
+ @failed.each_with_index do |failure, idx|
95
+ @output.puts " #{idx + 1}) #{failure.example.full_description}"
96
+ @output.puts @pastel.red(" #{failure.exception.message}")
97
+ if failure.exception.backtrace&.first
98
+ @output.puts @pastel.dim(" #{failure.exception.backtrace.first}")
99
+ end
100
+ @output.puts
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Candy
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "candy/version"
4
+ require_relative "candy/formatter"
5
+ require_relative "candy/coverage"
6
+
7
+ module RSpec
8
+ module Candy
9
+ class Error < StandardError; end
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ module Rspec
2
+ module Candy
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec-candy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Adam Ladachowski
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-02-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec-core
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pastel
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.8'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.8'
41
+ - !ruby/object:Gem::Dependency
42
+ name: tty-progressbar
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.18'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.18'
55
+ - !ruby/object:Gem::Dependency
56
+ name: tty-table
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.12'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.12'
69
+ - !ruby/object:Gem::Dependency
70
+ name: tty-screen
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.8'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.8'
83
+ description: A beautiful RSpec formatter with TTY progress bars, colored output, and
84
+ a stunning coverage report table.
85
+ email:
86
+ - adam.ladachowski@gmail.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - LICENSE.txt
92
+ - README.md
93
+ - Rakefile
94
+ - lib/rspec/candy.rb
95
+ - lib/rspec/candy/coverage.rb
96
+ - lib/rspec/candy/formatter.rb
97
+ - lib/rspec/candy/version.rb
98
+ - sig/rspec/candy.rbs
99
+ homepage: https://github.com/aladac/rspec-candy
100
+ licenses:
101
+ - MIT
102
+ metadata:
103
+ homepage_uri: https://github.com/aladac/rspec-candy
104
+ source_code_uri: https://github.com/aladac/rspec-candy
105
+ changelog_uri: https://github.com/aladac/rspec-candy/blob/main/CHANGELOG.md
106
+ post_install_message:
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: 3.2.0
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubygems_version: 3.5.22
122
+ signing_key:
123
+ specification_version: 4
124
+ summary: 'Eye candy for RSpec: pretty progress bars and coverage reports'
125
+ test_files: []