rutidy 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 +7 -0
- data/LICENSE +21 -0
- data/README.md +114 -0
- data/exe/rutidy +6 -0
- data/lib/rutidy/cli.rb +123 -0
- data/lib/rutidy/formatter.rb +112 -0
- data/lib/rutidy/render.rb +211 -0
- data/lib/rutidy/version.rb +5 -0
- data/lib/rutidy.rb +8 -0
- metadata +70 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e0d758de435d37cd2be15dc16e6865d0684de10a1c2e068e51ae2cf614dc7935
|
|
4
|
+
data.tar.gz: 527576fa67e3ce5f23660e7783f903daf23b4926b6c0905cdc03cd11eb48e8cb
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4170b31d9a055204e77c1db26d36f6c3c82b75e00e379cbda4be2fa2b22508b0274fb9699b29fa104fa7494947593fcf6f8c1914f592fcd8546285f6b820e82f
|
|
7
|
+
data.tar.gz: 300a3e45166a8188afc0a82660501c4dec4fa97d0edb819beae01df55a11d0442abc6edc64bbd3478e9b049a0516016f6317f58afd2fa7eea65cf9280ef80915
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 John Woodell
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# rutidy
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/rutidy)
|
|
4
|
+
[](https://github.com/woodie/rutidy/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/woodie/rutidy/releases/latest)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
RSpec/Mocha/Vitest-style output for RSpec by hooking a real formatter into
|
|
11
|
+
the same tree the runner already builds. See at a glance which spec
|
|
12
|
+
actually failed, in whichever of the four familiar looks reads best for
|
|
13
|
+
your team, without giving up RSpec's own assertions or your existing suite.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
gem install rutidy
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or add it to your `Gemfile`:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
group :test do
|
|
25
|
+
gem "rutidy"
|
|
26
|
+
end
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
Run as a wrapper around `rspec`, passing any arguments through:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
rutidy
|
|
35
|
+
rutidy spec/
|
|
36
|
+
rutidy -fd spec/calculator_spec.rb
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or read stdin, piping RSpec's own output through `rutidy`'s formatter first:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
rspec --require rutidy/formatter --format Rutidy::Formatter | rutidy -fs
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or format an existing report file directly:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
rutidy report.json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Flags select the output style -- see [Output styles](#output-styles) below.
|
|
52
|
+
|
|
53
|
+
### Version
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
rutidy --version
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Prints the installed version and exits immediately, without waiting on
|
|
60
|
+
stdin or running `rspec`.
|
|
61
|
+
|
|
62
|
+
## Output styles
|
|
63
|
+
|
|
64
|
+
Four named styles, each matching a convention from a familiar test runner.
|
|
65
|
+
The first three end with the same shared footer; `-fv` ends with Vitest's
|
|
66
|
+
own footer shape instead.
|
|
67
|
+
|
|
68
|
+
| Flag | Convention | Look |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| | Our base formatter | Glyph + `name (N seconds)`, failures add `(FAILED - N)` |
|
|
71
|
+
| -fd | RSpec's own doc format | Plain colored name, yellow `(PENDING: reason)` |
|
|
72
|
+
| -fs | Mocha's spec format | Green `✔` + gray name, red `✗ name (FAILED - N)` |
|
|
73
|
+
| -fv | Vitest's own tree | Green `✓ name`, two-toned green `2ms`, red `× name`, dim gray `↓ name` |
|
|
74
|
+
|
|
75
|
+
`-fv`'s `Test Files` line counts distinct `file_path`s across all examples --
|
|
76
|
+
exact, since every example already carries its own real spec file path.
|
|
77
|
+
|
|
78
|
+
## Things to know
|
|
79
|
+
|
|
80
|
+
### Why a real formatter, not a text post-processor
|
|
81
|
+
|
|
82
|
+
`gorderly`/`xctidy` parse `go test`/`xcodebuild`'s raw text output because
|
|
83
|
+
that's all those tools expose. RSpec is different: its own `--format json`
|
|
84
|
+
only reports a flattened `full_description` string (every `describe`/
|
|
85
|
+
`context`/`it` joined with plain spaces, no separator at all) with no
|
|
86
|
+
per-level boundaries -- reconstructing a real tree from that would mean
|
|
87
|
+
guessing where one group's text ends and the next begins, the exact
|
|
88
|
+
problem `xctidy` already has to solve for Quick/Nimble's comma-joined
|
|
89
|
+
names, except worse, since RSpec doesn't even delimit with commas.
|
|
90
|
+
|
|
91
|
+
`Rutidy::Formatter` sidesteps that entirely by hooking the same
|
|
92
|
+
`example_group_started`/`example_group_finished` notifications RSpec's own
|
|
93
|
+
`--format documentation` formatter uses internally -- the hierarchy comes
|
|
94
|
+
from the real stack the runner already maintains, not a guess.
|
|
95
|
+
|
|
96
|
+
### Limitations
|
|
97
|
+
|
|
98
|
+
- `Rutidy::Formatter` must run inside the same `rspec` process as your
|
|
99
|
+
suite (via `--require`/`--format`, or `rutidy`'s own CLI wrapping
|
|
100
|
+
`rspec` for you) -- it can't be retrofitted onto RSpec's own stock
|
|
101
|
+
`--format json` output after the fact.
|
|
102
|
+
- Shared examples (`it_behaves_like`) show up as their own nesting level
|
|
103
|
+
named after the shared group, matching what RSpec's own `--format
|
|
104
|
+
documentation` already does -- not a `rutidy`-specific quirk.
|
|
105
|
+
|
|
106
|
+
## Development
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
make build # gem build rutidy.gemspec
|
|
110
|
+
make install # rake install
|
|
111
|
+
make test # verbose, dogfoods rutidy on its own suite in -fd style
|
|
112
|
+
make lint # standardrb
|
|
113
|
+
make check # terse: silent on success, full log on failure
|
|
114
|
+
```
|
data/exe/rutidy
ADDED
data/lib/rutidy/cli.rb
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
require "rutidy/version"
|
|
6
|
+
require "rutidy/render"
|
|
7
|
+
|
|
8
|
+
module Rutidy
|
|
9
|
+
# `rutidy`'s two modes, matching `gomeleon`'s own "wrap the CLI, or read
|
|
10
|
+
# its JSON report directly" duality:
|
|
11
|
+
#
|
|
12
|
+
# rutidy # wraps `rspec` with no args
|
|
13
|
+
# rutidy spec/foo_spec.rb # wraps `rspec spec/foo_spec.rb`
|
|
14
|
+
# rutidy -fd spec/ # style flag + wrapped invocation
|
|
15
|
+
# rutidy report.json # formats an existing report file directly
|
|
16
|
+
# rspec ... | rutidy -fs # reads a piped report from stdin
|
|
17
|
+
#
|
|
18
|
+
# A single argument ending in `.json` is treated as an existing report to
|
|
19
|
+
# render; anything else on the command line is handed straight through to
|
|
20
|
+
# `rspec` (with `Formatter` inserted via `--require`/`--format`). Piped
|
|
21
|
+
# stdin with no arguments at all is read as a report too, the same way
|
|
22
|
+
# `gorderly`'s `openInput` treats piped stdin vs. shelling out to `go test`
|
|
23
|
+
# itself.
|
|
24
|
+
class CLI
|
|
25
|
+
STYLE_ALIASES = {
|
|
26
|
+
"documentation" => :fd,
|
|
27
|
+
"spec" => :fs,
|
|
28
|
+
"vitest" => :fv
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
def self.run(argv, out: $stdout, err: $stderr, stdin: $stdin)
|
|
32
|
+
new(argv, out: out, err: err, stdin: stdin).run
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(argv, out:, err:, stdin:)
|
|
36
|
+
@argv = argv.dup
|
|
37
|
+
@out = out
|
|
38
|
+
@err = err
|
|
39
|
+
@stdin = stdin
|
|
40
|
+
@style = :classic
|
|
41
|
+
@color = @out.respond_to?(:tty?) && @out.tty? && ENV["NO_COLOR"].nil?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def run
|
|
45
|
+
if wants_version?
|
|
46
|
+
@out.puts("rutidy #{Rutidy::VERSION}")
|
|
47
|
+
return 0
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
rest = parse_flags!
|
|
51
|
+
examples = load_examples(rest)
|
|
52
|
+
return 1 if examples.nil?
|
|
53
|
+
|
|
54
|
+
Render.call(examples, style: @style, out: @out, color: @color).positive? ? 1 : 0
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def wants_version?
|
|
60
|
+
@argv.include?("--version") || @argv.include?("-v")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def parse_flags!
|
|
64
|
+
rest = []
|
|
65
|
+
@argv.each do |arg|
|
|
66
|
+
case arg
|
|
67
|
+
when "-fd" then @style = :fd
|
|
68
|
+
when "-fs" then @style = :fs
|
|
69
|
+
when "-fv" then @style = :fv
|
|
70
|
+
when /\A--format[= ](\w+)\z/
|
|
71
|
+
@style = STYLE_ALIASES.fetch(Regexp.last_match(1)) do
|
|
72
|
+
@err.puts("rutidy: unknown --format #{Regexp.last_match(1)}")
|
|
73
|
+
@style
|
|
74
|
+
end
|
|
75
|
+
else rest << arg
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
rest
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def load_examples(rest)
|
|
82
|
+
if rest.length == 1 && rest.first.end_with?(".json")
|
|
83
|
+
return parse_report(File.read(rest.first))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
if rest.empty? && !@stdin.tty?
|
|
87
|
+
return parse_report(@stdin.read)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
run_rspec(rest)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def parse_report(json_text)
|
|
94
|
+
JSON.parse(json_text, symbolize_names: true).fetch(:examples, [])
|
|
95
|
+
rescue JSON::ParserError => e
|
|
96
|
+
@err.puts("rutidy: couldn't parse report: #{e.message}")
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def run_rspec(args)
|
|
101
|
+
Tempfile.create(["rutidy", ".json"]) do |tmp|
|
|
102
|
+
cmd = [
|
|
103
|
+
"rspec", "--require", "rutidy/formatter", "--format", "Rutidy::Formatter",
|
|
104
|
+
"--out", tmp.path, *args
|
|
105
|
+
]
|
|
106
|
+
# `rspec` exits non-zero whenever any example fails -- that's the
|
|
107
|
+
# normal case this tool exists to render, not an error. Only the
|
|
108
|
+
# report ending up empty (rspec never got far enough to write one --
|
|
109
|
+
# a load error, an unrecognized flag, no matching spec files) is a
|
|
110
|
+
# real problem worth surfacing here.
|
|
111
|
+
system(*cmd, out: @err, err: @err)
|
|
112
|
+
|
|
113
|
+
content = File.read(tmp.path)
|
|
114
|
+
if content.strip.empty?
|
|
115
|
+
@err.puts("rutidy: `#{cmd.join(" ")}` produced no report -- see rspec's own output above")
|
|
116
|
+
return nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
parse_report(content)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "rspec/core/formatters"
|
|
5
|
+
require "rspec/core/formatters/base_formatter"
|
|
6
|
+
require "rutidy/version"
|
|
7
|
+
|
|
8
|
+
module Rutidy
|
|
9
|
+
# A real RSpec formatter (registered the same way RSpec's own built-in
|
|
10
|
+
# DocumentationFormatter is), not a post-processor of some other
|
|
11
|
+
# formatter's text output. `example_group_started`/`example_group_finished`
|
|
12
|
+
# fire as the runner actually descends/ascends the real describe/context
|
|
13
|
+
# tree, so `@hierarchy` always reflects genuine nesting -- unlike stock
|
|
14
|
+
# `rspec --format json`, whose `full_description` field is every group's
|
|
15
|
+
# description concatenated with plain spaces and no separator, which
|
|
16
|
+
# can't be split back into a tree without guessing. This formatter never
|
|
17
|
+
# needs to guess: every example gets its own `hierarchy` array straight
|
|
18
|
+
# from the stack RSpec itself maintains.
|
|
19
|
+
#
|
|
20
|
+
# Load with `rspec --require rutidy/formatter --format Rutidy::Formatter`,
|
|
21
|
+
# or let `rutidy`'s own CLI wrap that invocation for you.
|
|
22
|
+
class Formatter < RSpec::Core::Formatters::BaseFormatter
|
|
23
|
+
RSpec::Core::Formatters.register self,
|
|
24
|
+
:example_group_started, :example_group_finished,
|
|
25
|
+
:example_passed, :example_failed, :example_pending,
|
|
26
|
+
:dump_summary, :seed, :message, :close
|
|
27
|
+
|
|
28
|
+
def initialize(output)
|
|
29
|
+
super
|
|
30
|
+
@hierarchy = []
|
|
31
|
+
@output_hash = {version: Rutidy::VERSION}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def message(notification)
|
|
35
|
+
(@output_hash[:messages] ||= []) << notification.message
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def example_group_started(notification)
|
|
39
|
+
@hierarchy << notification.group.description.strip
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def example_group_finished(_notification)
|
|
43
|
+
@hierarchy.pop
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def example_passed(notification)
|
|
47
|
+
record(notification.example)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def example_pending(notification)
|
|
51
|
+
record(notification.example)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def example_failed(notification)
|
|
55
|
+
record(notification.example, notification)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def dump_summary(summary)
|
|
59
|
+
@output_hash[:summary] = {
|
|
60
|
+
duration: summary.duration,
|
|
61
|
+
example_count: summary.example_count,
|
|
62
|
+
failure_count: summary.failure_count,
|
|
63
|
+
pending_count: summary.pending_count
|
|
64
|
+
}
|
|
65
|
+
@output_hash[:summary_line] = summary.totals_line
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def seed(notification)
|
|
69
|
+
return unless notification.seed_used?
|
|
70
|
+
|
|
71
|
+
@output_hash[:seed] = notification.seed
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def close(_notification)
|
|
75
|
+
output.write JSON.generate(@output_hash)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def record(example, failure_notification = nil)
|
|
81
|
+
(@output_hash[:examples] ||= []) << format_example(example, failure_notification)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Same field set stock `rspec --format json` gives you (description,
|
|
85
|
+
# full_description, status, file_path, line_number, run_time,
|
|
86
|
+
# pending_message, exception) plus the one field it's missing:
|
|
87
|
+
# `hierarchy`, the real per-level nesting `render.rb` builds a tree from.
|
|
88
|
+
def format_example(example, failure_notification)
|
|
89
|
+
result = example.execution_result
|
|
90
|
+
hash = {
|
|
91
|
+
hierarchy: @hierarchy + [example.description.strip],
|
|
92
|
+
description: example.description,
|
|
93
|
+
full_description: example.full_description,
|
|
94
|
+
status: result.status.to_s,
|
|
95
|
+
file_path: example.metadata[:file_path],
|
|
96
|
+
line_number: example.metadata[:line_number],
|
|
97
|
+
run_time: result.run_time
|
|
98
|
+
}
|
|
99
|
+
hash[:pending_message] = result.pending_message if result.pending_message
|
|
100
|
+
hash[:exception] = format_exception(example.exception, failure_notification) if failure_notification
|
|
101
|
+
hash
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def format_exception(exception, failure_notification)
|
|
105
|
+
{
|
|
106
|
+
class: exception.class.name,
|
|
107
|
+
message: exception.message,
|
|
108
|
+
backtrace: failure_notification.formatted_backtrace
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rutidy
|
|
4
|
+
# Walks the leaves `Formatter` (or a real `rspec --format json` report
|
|
5
|
+
# extended with a `hierarchy` array) produced and renders them as a nested
|
|
6
|
+
# RSpec `-fd`-style tree, the same dedupe-shared-prefix walk `gorderly`'s
|
|
7
|
+
# `render.go` and `xctidy`'s `Engine.swift` use, just over each example's
|
|
8
|
+
# real `hierarchy` array instead of a `/`-split or comma-disambiguated
|
|
9
|
+
# string. Four styles (`:classic`/`:fd`/`:fs`/`:fv`) share one closing
|
|
10
|
+
# footer, except `:fv`, which closes with Vitest's own `Test Files`/
|
|
11
|
+
# `Tests`/`Duration` shape.
|
|
12
|
+
#
|
|
13
|
+
# Unlike `gorderly`, there's no fake "package path" label above the tree --
|
|
14
|
+
# RSpec has no equivalent concept, so the first top-level `describe` is
|
|
15
|
+
# just the first real node. A blank line still goes before every
|
|
16
|
+
# top-level group, including the first -- that's not a `gorderly`-style
|
|
17
|
+
# workaround here, it's genuine RSpec `-fd` behavior (its own
|
|
18
|
+
# `DocumentationFormatter#example_group_started` does `output.puts if
|
|
19
|
+
# @group_level == 0` unconditionally).
|
|
20
|
+
module Render
|
|
21
|
+
ANSI_PREFIX = "\e["
|
|
22
|
+
COLORS = {
|
|
23
|
+
red: "31",
|
|
24
|
+
green: "32",
|
|
25
|
+
bright_green: "92",
|
|
26
|
+
yellow: "33",
|
|
27
|
+
cyan: "36",
|
|
28
|
+
gray: "90"
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
FailureEntry = Struct.new(:n, :full, :output)
|
|
32
|
+
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
# @return [Integer] the number of failed examples (0 means a clean run)
|
|
36
|
+
def call(examples, style:, out:, color: true)
|
|
37
|
+
colorize = ->(name, text) { color ? "#{ANSI_PREFIX}#{COLORS.fetch(name)}m#{text}#{ANSI_PREFIX}0m" : text }
|
|
38
|
+
|
|
39
|
+
total = 0
|
|
40
|
+
pending = 0
|
|
41
|
+
failed_count = 0
|
|
42
|
+
total_elapsed = 0.0
|
|
43
|
+
failures = []
|
|
44
|
+
files_total = {}
|
|
45
|
+
files_failed = {}
|
|
46
|
+
prev_path = []
|
|
47
|
+
|
|
48
|
+
examples.each do |example|
|
|
49
|
+
hierarchy = example.fetch(:hierarchy)
|
|
50
|
+
path = hierarchy[0..-2]
|
|
51
|
+
leaf_name = hierarchy.last
|
|
52
|
+
|
|
53
|
+
shared = 0
|
|
54
|
+
shared += 1 while shared < prev_path.length && shared < path.length && prev_path[shared] == path[shared]
|
|
55
|
+
|
|
56
|
+
out.puts if shared.zero?
|
|
57
|
+
(shared...path.length).each { |i| out.puts("#{" " * i}#{path[i]}") }
|
|
58
|
+
|
|
59
|
+
total += 1
|
|
60
|
+
total_elapsed += example[:run_time]
|
|
61
|
+
indent = " " * path.length
|
|
62
|
+
file = example[:file_path]
|
|
63
|
+
files_total[file] = true if file
|
|
64
|
+
|
|
65
|
+
case example[:status]
|
|
66
|
+
when "failed"
|
|
67
|
+
failed_count += 1
|
|
68
|
+
files_failed[file] = true if file
|
|
69
|
+
n = failures.length + 1
|
|
70
|
+
failures << FailureEntry.new(n, hierarchy, format_backtrace(example))
|
|
71
|
+
out.puts("#{indent}#{colorize_fail(style, leaf_name, example, n, colorize)}")
|
|
72
|
+
when "pending"
|
|
73
|
+
pending += 1
|
|
74
|
+
out.puts("#{indent}#{colorize_pending(style, leaf_name, example, colorize)}")
|
|
75
|
+
else
|
|
76
|
+
out.puts("#{indent}#{colorize_pass(style, leaf_name, example, colorize)}")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
prev_path = path
|
|
80
|
+
end
|
|
81
|
+
out.puts
|
|
82
|
+
|
|
83
|
+
if failures.any?
|
|
84
|
+
out.puts("Failures:")
|
|
85
|
+
failures.each do |f|
|
|
86
|
+
out.puts
|
|
87
|
+
out.puts(" #{f.n}) #{f.full.join(" ")}")
|
|
88
|
+
f.output.each { |line| out.puts(" #{line}") }
|
|
89
|
+
end
|
|
90
|
+
out.puts
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
if style == :fv
|
|
94
|
+
write_vitest_footer(
|
|
95
|
+
out, colorize,
|
|
96
|
+
total: total, failed_count: failed_count, pending: pending, total_elapsed: total_elapsed,
|
|
97
|
+
files_total: files_total.size, files_failed: files_failed.size
|
|
98
|
+
)
|
|
99
|
+
else
|
|
100
|
+
write_shared_footer(
|
|
101
|
+
out, colorize,
|
|
102
|
+
total: total, failed_count: failed_count, pending: pending, total_elapsed: total_elapsed
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
failed_count
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def write_shared_footer(out, colorize, total:, failed_count:, pending:, total_elapsed:)
|
|
110
|
+
verdict = failed_count.positive? ? "Test Failed" : "Test Succeeded"
|
|
111
|
+
verdict_color = failed_count.positive? ? :red : :green
|
|
112
|
+
out.puts(colorize.call(verdict_color, verdict))
|
|
113
|
+
out.puts(colorize.call(
|
|
114
|
+
verdict_color,
|
|
115
|
+
format("Tests Passed: %d failed, %d pending, %d total (%s seconds)",
|
|
116
|
+
failed_count, pending, total, format_seconds(total_elapsed))
|
|
117
|
+
))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def write_vitest_footer(out, colorize, total:, failed_count:, pending:, total_elapsed:, files_total:, files_failed:)
|
|
121
|
+
passed = total - failed_count - pending
|
|
122
|
+
files_passed = files_total - files_failed
|
|
123
|
+
out.puts(vitest_summary_line("Test Files", colorize, failed: files_failed, passed: files_passed, pending: 0, total: files_total))
|
|
124
|
+
out.puts(vitest_summary_line("Tests", colorize, failed: failed_count, passed: passed, pending: pending, total: total))
|
|
125
|
+
out.puts(format("%11s %s", "Duration", format_vitest_duration(total_elapsed)))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def vitest_summary_line(label, colorize, failed:, passed:, pending:, total:)
|
|
129
|
+
parts = []
|
|
130
|
+
parts << colorize.call(:red, "#{failed} failed") if failed.positive?
|
|
131
|
+
parts << colorize.call(:green, "#{passed} passed") if passed.positive?
|
|
132
|
+
parts << colorize.call(:gray, "#{pending} pending") if pending.positive?
|
|
133
|
+
parts << "0 passed" if parts.empty?
|
|
134
|
+
format("%11s %s (%d)", label, parts.join(" | "), total)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def colorize_pass(style, name, example, colorize)
|
|
138
|
+
case style
|
|
139
|
+
when :classic
|
|
140
|
+
"#{colorize.call(:green, "✔")} #{name} (#{colorize.call(:green, format_seconds(example[:run_time]))} seconds)"
|
|
141
|
+
when :fs
|
|
142
|
+
"#{colorize.call(:green, "✔")} #{colorize.call(:gray, name)}"
|
|
143
|
+
when :fv
|
|
144
|
+
number, unit = format_vitest_duration_parts(example[:run_time])
|
|
145
|
+
"#{colorize.call(:green, "✓")} #{name} #{colorize.call(:green, number)}#{colorize.call(:bright_green, unit)}"
|
|
146
|
+
else # :fd
|
|
147
|
+
colorize.call(:green, name)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def colorize_fail(style, name, example, n, colorize)
|
|
152
|
+
case style
|
|
153
|
+
when :classic
|
|
154
|
+
"#{colorize.call(:red, "✖")} #{name} (FAILED - #{n}) (#{colorize.call(:red, format_seconds(example[:run_time]))} seconds)"
|
|
155
|
+
when :fs
|
|
156
|
+
colorize.call(:red, "✗ #{name} (FAILED - #{n})")
|
|
157
|
+
when :fv
|
|
158
|
+
colorize.call(:red, "× #{name} #{format_vitest_duration(example[:run_time])}")
|
|
159
|
+
else # :fd
|
|
160
|
+
colorize.call(:red, "#{name} (FAILED - #{n})")
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# RSpec's own vocabulary is "pending" -- not "skipped" the way Go/Ginkgo
|
|
165
|
+
# call it -- so every style says PENDING, matching what `rspec`'s real
|
|
166
|
+
# `-fd`/`--format documentation` would say for the same example. Include
|
|
167
|
+
# the pending reason where RSpec itself would (`-fd`/`-fs`); `:classic`
|
|
168
|
+
# and `:fv` stay terse (glyph/name/time only), matching how `gorderly`'s
|
|
169
|
+
# classic and Vitest's own tree both treat a skip.
|
|
170
|
+
def colorize_pending(style, name, example, colorize)
|
|
171
|
+
case style
|
|
172
|
+
when :classic
|
|
173
|
+
"#{colorize.call(:cyan, "⊘")} #{name} (#{colorize.call(:cyan, format_seconds(example[:run_time]))} seconds)"
|
|
174
|
+
when :fs
|
|
175
|
+
colorize.call(:cyan, "- #{name} #{pending_suffix(example)}")
|
|
176
|
+
when :fv
|
|
177
|
+
colorize.call(:gray, "↓ #{name}")
|
|
178
|
+
else # :fd
|
|
179
|
+
colorize.call(:yellow, "#{name} #{pending_suffix(example)}")
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def pending_suffix(example)
|
|
184
|
+
reason = example[:pending_message]
|
|
185
|
+
reason ? "(PENDING: #{reason})" : "(PENDING)"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def format_backtrace(example)
|
|
189
|
+
exception = example[:exception]
|
|
190
|
+
return [] unless exception
|
|
191
|
+
|
|
192
|
+
["#{exception[:class]}: #{exception[:message]}"] + Array(exception[:backtrace])
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def format_seconds(seconds)
|
|
196
|
+
(seconds < 1) ? format("%.4f", seconds) : format("%.2f", seconds)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def format_vitest_duration_parts(seconds)
|
|
200
|
+
ms = seconds * 1000
|
|
201
|
+
return [format("%.2f", ms / 1000), "s"] if ms > 1000
|
|
202
|
+
|
|
203
|
+
[ms.round.to_s, "ms"]
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def format_vitest_duration(seconds)
|
|
207
|
+
number, unit = format_vitest_duration_parts(seconds)
|
|
208
|
+
"#{number}#{unit}"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
data/lib/rutidy.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rutidy
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- John Woodell
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-03 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rspec
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.13'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '3.13'
|
|
27
|
+
description: A real RSpec formatter plus a small renderer that turns RSpec's own example
|
|
28
|
+
tree into the same classic/fd/fs/fv styles gorderly, kotidy, and xctidy already
|
|
29
|
+
share.
|
|
30
|
+
email:
|
|
31
|
+
- woodie@netpress.com
|
|
32
|
+
executables:
|
|
33
|
+
- rutidy
|
|
34
|
+
extensions: []
|
|
35
|
+
extra_rdoc_files: []
|
|
36
|
+
files:
|
|
37
|
+
- LICENSE
|
|
38
|
+
- README.md
|
|
39
|
+
- exe/rutidy
|
|
40
|
+
- lib/rutidy.rb
|
|
41
|
+
- lib/rutidy/cli.rb
|
|
42
|
+
- lib/rutidy/formatter.rb
|
|
43
|
+
- lib/rutidy/render.rb
|
|
44
|
+
- lib/rutidy/version.rb
|
|
45
|
+
homepage: https://github.com/woodie/rutidy
|
|
46
|
+
licenses:
|
|
47
|
+
- MIT
|
|
48
|
+
metadata:
|
|
49
|
+
homepage_uri: https://github.com/woodie/rutidy
|
|
50
|
+
source_code_uri: https://github.com/woodie/rutidy
|
|
51
|
+
post_install_message:
|
|
52
|
+
rdoc_options: []
|
|
53
|
+
require_paths:
|
|
54
|
+
- lib
|
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '3.0'
|
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: '0'
|
|
65
|
+
requirements: []
|
|
66
|
+
rubygems_version: 3.5.22
|
|
67
|
+
signing_key:
|
|
68
|
+
specification_version: 4
|
|
69
|
+
summary: RSpec/Mocha/Vitest-style output for RSpec
|
|
70
|
+
test_files: []
|