minitest-heat 1.2.0 → 2.0.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 +4 -4
- data/.github/workflows/main.yml +79 -8
- data/.github/workflows/release.yml +98 -0
- data/.rubocop.yml +7 -2
- data/CHANGELOG.md +33 -1
- data/Gemfile +2 -1
- data/Gemfile.lock +70 -39
- data/README.md +88 -6
- data/RELEASING.md +190 -0
- data/Rakefile +117 -0
- data/lib/minitest/heat/backtrace/line_count.rb +6 -3
- data/lib/minitest/heat/backtrace/line_parser.rb +3 -1
- data/lib/minitest/heat/backtrace.rb +1 -1
- data/lib/minitest/heat/hit.rb +30 -14
- data/lib/minitest/heat/issue.rb +29 -1
- data/lib/minitest/heat/location.rb +13 -0
- data/lib/minitest/heat/map.rb +7 -0
- data/lib/minitest/heat/output/backtrace.rb +2 -1
- data/lib/minitest/heat/output/issue.rb +12 -5
- data/lib/minitest/heat/output/results.rb +6 -1
- data/lib/minitest/heat/output/token.rb +2 -2
- data/lib/minitest/heat/output.rb +3 -2
- data/lib/minitest/heat/results.rb +41 -8
- data/lib/minitest/heat/source.rb +4 -3
- data/lib/minitest/heat/timer.rb +13 -0
- data/lib/minitest/heat/version.rb +1 -1
- data/lib/minitest/heat_plugin.rb +6 -0
- data/lib/minitest/heat_reporter.rb +44 -8
- data/minitest-heat.gemspec +3 -2
- metadata +13 -14
- data/.travis.yml +0 -6
data/RELEASING.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Releasing Minitest Heat
|
|
2
|
+
|
|
3
|
+
## Quick Reference
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
# 1. Update version and changelog
|
|
7
|
+
# 2. Commit and push
|
|
8
|
+
git add -A && git commit -m "Release vX.Y.Z" && git push
|
|
9
|
+
|
|
10
|
+
# 3. Tag and push
|
|
11
|
+
git tag vX.Y.Z && git push origin vX.Y.Z
|
|
12
|
+
|
|
13
|
+
# Done - automation handles the rest
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## How It Works
|
|
17
|
+
|
|
18
|
+
Releases are fully automated via GitHub Actions:
|
|
19
|
+
|
|
20
|
+
1. **Branch protection** requires CI to pass before merging to `main`
|
|
21
|
+
2. When you push a version tag, the release workflow:
|
|
22
|
+
- Validates the tag points to a commit on `main` (ensures CI passed)
|
|
23
|
+
- Builds and publishes the gem to RubyGems
|
|
24
|
+
- Creates a GitHub Release with changelog excerpt
|
|
25
|
+
|
|
26
|
+
No redundant test runs. If it's on `main`, it already passed CI.
|
|
27
|
+
|
|
28
|
+
## Release Steps
|
|
29
|
+
|
|
30
|
+
### 1. Update Version
|
|
31
|
+
|
|
32
|
+
Edit `lib/minitest/heat/version.rb`:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
VERSION = 'X.Y.Z'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Update CHANGELOG
|
|
39
|
+
|
|
40
|
+
Move items from `[Unreleased]` to a new version section:
|
|
41
|
+
|
|
42
|
+
```markdown
|
|
43
|
+
## [Unreleased]
|
|
44
|
+
|
|
45
|
+
## [X.Y.Z] - YYYY-MM-DD
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
- New feature description
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
- Bug fix description
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 3. Commit, Tag, Push
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git add lib/minitest/heat/version.rb CHANGELOG.md
|
|
58
|
+
git commit -m "Release vX.Y.Z"
|
|
59
|
+
git push origin main
|
|
60
|
+
git tag vX.Y.Z
|
|
61
|
+
git push origin vX.Y.Z
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 4. Verify
|
|
65
|
+
|
|
66
|
+
- Watch the [Actions tab](https://github.com/garrettdimon/minitest-heat/actions) for workflow completion
|
|
67
|
+
- Check [RubyGems](https://rubygems.org/gems/minitest-heat) for the new version
|
|
68
|
+
- Check [GitHub Releases](https://github.com/garrettdimon/minitest-heat/releases) for the release page
|
|
69
|
+
|
|
70
|
+
## Versioning Policy
|
|
71
|
+
|
|
72
|
+
Follow [Semantic Versioning](https://semver.org/):
|
|
73
|
+
|
|
74
|
+
- **MAJOR** (x.0.0): Breaking changes to public API or configuration
|
|
75
|
+
- **MINOR** (0.x.0): New features, deprecations
|
|
76
|
+
- **PATCH** (0.0.x): Bug fixes, documentation
|
|
77
|
+
|
|
78
|
+
### What's a Breaking Change?
|
|
79
|
+
|
|
80
|
+
- Removing or renaming public classes/methods
|
|
81
|
+
- Changing method signatures incompatibly
|
|
82
|
+
- Changing default configuration behavior
|
|
83
|
+
- Dropping Ruby version support
|
|
84
|
+
|
|
85
|
+
## Local Tools
|
|
86
|
+
|
|
87
|
+
### Full Preflight Check
|
|
88
|
+
|
|
89
|
+
Run all checks before pushing:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
bundle exec rake release:preflight
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
This runs tests, security audit, and release validation in sequence.
|
|
96
|
+
|
|
97
|
+
### Individual Tasks
|
|
98
|
+
|
|
99
|
+
| Task | Purpose |
|
|
100
|
+
|------|---------|
|
|
101
|
+
| `rake release:preflight` | Run all checks (test, audit, check) |
|
|
102
|
+
| `rake release:check` | Validate version format, changelog entry, git state |
|
|
103
|
+
| `rake release:audit` | Check for vulnerable dependencies |
|
|
104
|
+
| `rake release:dry_run` | Build gem locally, show contents and size |
|
|
105
|
+
| `rake test` | Run test suite |
|
|
106
|
+
|
|
107
|
+
### Preview a Release
|
|
108
|
+
|
|
109
|
+
Before tagging, preview what the gem will contain:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
bundle exec rake release:dry_run
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
This builds the gem, displays its contents and size, then cleans up.
|
|
116
|
+
|
|
117
|
+
## One-Time Setup
|
|
118
|
+
|
|
119
|
+
### RubyGems Trusted Publishing
|
|
120
|
+
|
|
121
|
+
1. Go to [rubygems.org/profile/oidc/pending_trusted_publishers](https://rubygems.org/profile/oidc/pending_trusted_publishers)
|
|
122
|
+
2. Add trusted publisher:
|
|
123
|
+
- **Gem name:** `minitest-heat`
|
|
124
|
+
- **Repository owner:** `garrettdimon`
|
|
125
|
+
- **Repository name:** `minitest-heat`
|
|
126
|
+
- **Workflow filename:** `release.yml`
|
|
127
|
+
- **Environment:** `rubygems`
|
|
128
|
+
|
|
129
|
+
### GitHub Environment
|
|
130
|
+
|
|
131
|
+
1. Go to repository Settings > Environments
|
|
132
|
+
2. Create environment named `rubygems`
|
|
133
|
+
3. (Optional) Add required reviewers for extra safety
|
|
134
|
+
|
|
135
|
+
### Repository Ruleset
|
|
136
|
+
|
|
137
|
+
1. Go to repository Settings > Rules > Rulesets
|
|
138
|
+
2. Edit the `main` ruleset (or create one targeting the default branch)
|
|
139
|
+
3. Enable "Require status checks to pass" with these checks:
|
|
140
|
+
- `Security`
|
|
141
|
+
- `Test (Ruby 3.2)`
|
|
142
|
+
- `Test (Ruby 3.3)`
|
|
143
|
+
- `Test (Ruby 3.4)`
|
|
144
|
+
- `Test (Ruby 4.0)`
|
|
145
|
+
- `Changelog`
|
|
146
|
+
- `Version`
|
|
147
|
+
|
|
148
|
+
## Troubleshooting
|
|
149
|
+
|
|
150
|
+
### Release workflow fails with "must point to commit on main"
|
|
151
|
+
|
|
152
|
+
You tagged a commit that isn't on the main branch. Delete the tag and re-tag a commit on main:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
git tag -d vX.Y.Z # Delete local tag
|
|
156
|
+
git push origin :vX.Y.Z # Delete remote tag
|
|
157
|
+
git checkout main
|
|
158
|
+
git pull
|
|
159
|
+
git tag vX.Y.Z
|
|
160
|
+
git push origin vX.Y.Z
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### "You do not have permission to push to this gem"
|
|
164
|
+
|
|
165
|
+
The RubyGems trusted publisher isn't configured, or the environment name doesn't match. Check the one-time setup steps above.
|
|
166
|
+
|
|
167
|
+
### Forgot to update CHANGELOG
|
|
168
|
+
|
|
169
|
+
Delete the tag, update CHANGELOG, amend the commit, re-tag:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
git tag -d vX.Y.Z
|
|
173
|
+
git push origin :vX.Y.Z
|
|
174
|
+
# Update CHANGELOG.md
|
|
175
|
+
git add CHANGELOG.md
|
|
176
|
+
git commit --amend --no-edit
|
|
177
|
+
git push --force-with-lease origin main
|
|
178
|
+
git tag vX.Y.Z
|
|
179
|
+
git push origin vX.Y.Z
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Manual Release (Fallback)
|
|
183
|
+
|
|
184
|
+
If automation fails and you need to publish manually:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
bundle exec rake release
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
This builds the gem and pushes to RubyGems using your local credentials.
|
data/Rakefile
CHANGED
|
@@ -10,3 +10,120 @@ Rake::TestTask.new(:test) do |t|
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
task default: :test
|
|
13
|
+
|
|
14
|
+
# rubocop:disable Metrics/BlockLength
|
|
15
|
+
namespace :release do
|
|
16
|
+
desc 'Run bundle-audit to check for vulnerable dependencies'
|
|
17
|
+
task :audit do
|
|
18
|
+
puts 'Running security audit...'
|
|
19
|
+
sh 'bundle exec bundle-audit check --update'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc 'Validate version, changelog, and git state before release'
|
|
23
|
+
task :check do
|
|
24
|
+
require_relative 'lib/minitest/heat/version'
|
|
25
|
+
errors = ReleaseChecker.new(Minitest::Heat::VERSION).validate
|
|
26
|
+
if errors.any?
|
|
27
|
+
puts "\nRelease check failed:"
|
|
28
|
+
errors.each { |e| puts " - #{e}" }
|
|
29
|
+
exit 1
|
|
30
|
+
else
|
|
31
|
+
puts 'All release checks passed.'
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
desc 'Run all pre-release checks (tests, audit, release:check)'
|
|
36
|
+
task preflight: %i[test audit check] do
|
|
37
|
+
puts "\nAll preflight checks passed. Ready to release."
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
desc 'Build gem locally and show contents (dry run)'
|
|
41
|
+
task :dry_run do
|
|
42
|
+
require_relative 'lib/minitest/heat/version'
|
|
43
|
+
DryRun.new(Minitest::Heat::VERSION).run
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
# rubocop:enable Metrics/BlockLength
|
|
47
|
+
|
|
48
|
+
# Validates release readiness
|
|
49
|
+
class ReleaseChecker
|
|
50
|
+
def initialize(version)
|
|
51
|
+
@version = version
|
|
52
|
+
@errors = []
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def validate
|
|
56
|
+
puts "Checking release readiness for v#{@version}..."
|
|
57
|
+
check_version_format
|
|
58
|
+
check_changelog
|
|
59
|
+
check_git_clean
|
|
60
|
+
check_main_branch
|
|
61
|
+
@errors
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def check_version_format
|
|
67
|
+
return if @version.match?(/\A\d+\.\d+\.\d+\z/)
|
|
68
|
+
|
|
69
|
+
@errors << "Version '#{@version}' is not valid semver (expected X.Y.Z)"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def check_changelog
|
|
73
|
+
changelog = File.read('CHANGELOG.md')
|
|
74
|
+
return if changelog.include?("[#{@version}]")
|
|
75
|
+
|
|
76
|
+
@errors << "CHANGELOG.md has no entry for version #{@version}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def check_git_clean
|
|
80
|
+
return if `git status --porcelain`.empty?
|
|
81
|
+
|
|
82
|
+
@errors << 'Working directory has uncommitted changes'
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def check_main_branch
|
|
86
|
+
current_branch = `git branch --show-current`.strip
|
|
87
|
+
return if current_branch == 'main'
|
|
88
|
+
|
|
89
|
+
@errors << "Not on main branch (currently on '#{current_branch}')"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Builds gem and displays contents without publishing
|
|
94
|
+
class DryRun
|
|
95
|
+
def initialize(version)
|
|
96
|
+
@version = version
|
|
97
|
+
@gem_file = "minitest-heat-#{version}.gem"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def run
|
|
101
|
+
build || abort('Gem build failed')
|
|
102
|
+
show_contents
|
|
103
|
+
show_size
|
|
104
|
+
ensure
|
|
105
|
+
cleanup if File.exist?(@gem_file)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def build
|
|
111
|
+
puts "Building #{@gem_file}..."
|
|
112
|
+
system 'gem build minitest-heat.gemspec --silent'
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def show_contents
|
|
116
|
+
puts "\nGem contents:"
|
|
117
|
+
system "tar -tf #{@gem_file}"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def show_size
|
|
121
|
+
size = File.size(@gem_file)
|
|
122
|
+
puts "\nGem size: #{(size / 1024.0).round(1)} KB"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def cleanup
|
|
126
|
+
File.delete(@gem_file)
|
|
127
|
+
puts "Cleaned up #{@gem_file}"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -19,15 +19,18 @@ module Minitest
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def max_location
|
|
22
|
-
locations.size - 1
|
|
22
|
+
[locations.size - 1, 0].max
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def limit
|
|
26
|
+
return 0 if locations.empty?
|
|
27
|
+
|
|
28
|
+
# Find the minimum relevant index, then add 1 to convert from index to count
|
|
26
29
|
[
|
|
27
|
-
DEFAULT_LINE_COUNT,
|
|
30
|
+
DEFAULT_LINE_COUNT - 1,
|
|
28
31
|
earliest_project_location,
|
|
29
32
|
max_location
|
|
30
|
-
].compact.min
|
|
33
|
+
].compact.min + 1
|
|
31
34
|
end
|
|
32
35
|
end
|
|
33
36
|
end
|
|
@@ -10,7 +10,9 @@ module Minitest
|
|
|
10
10
|
module LineParser
|
|
11
11
|
# Parses a line from a backtrace in order to convert it to usable components
|
|
12
12
|
def self.read(raw_text)
|
|
13
|
-
|
|
13
|
+
return nil if raw_text.nil? || raw_text.empty?
|
|
14
|
+
|
|
15
|
+
raw_pathname, raw_line_number, raw_container = raw_text.to_s.split(':')
|
|
14
16
|
raw_container = raw_container&.delete_prefix('in `')&.delete_suffix("'")
|
|
15
17
|
|
|
16
18
|
::Minitest::Heat::Location.new(
|
|
@@ -31,7 +31,7 @@ module Minitest
|
|
|
31
31
|
def locations
|
|
32
32
|
return [] if raw_backtrace.nil?
|
|
33
33
|
|
|
34
|
-
@locations ||= raw_backtrace.map { |entry| Backtrace::LineParser.read(entry) }
|
|
34
|
+
@locations ||= raw_backtrace.map { |entry| Backtrace::LineParser.read(entry) }.compact
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# All entries from the backtrace within the project and sorted with the most recently modified
|
data/lib/minitest/heat/hit.rb
CHANGED
|
@@ -63,33 +63,49 @@ module Minitest
|
|
|
63
63
|
#
|
|
64
64
|
# @return [Integer] the problem weight for the file
|
|
65
65
|
def weight
|
|
66
|
-
|
|
67
|
-
issues.each_pair do |type, values|
|
|
68
|
-
weight += values.size * WEIGHTS.fetch(type, 0)
|
|
69
|
-
end
|
|
70
|
-
weight
|
|
66
|
+
issues.sum { |type, values| values.size * WEIGHTS.fetch(type, 0) }
|
|
71
67
|
end
|
|
72
68
|
|
|
73
69
|
# The total issue count for the file across all issue types. Includes duplicates if they exist
|
|
74
70
|
#
|
|
75
71
|
# @return [Integer] the sum of the counts for all line numbers for all issue types
|
|
76
72
|
def count
|
|
77
|
-
|
|
78
|
-
issues.each_pair do |_type, values|
|
|
79
|
-
count += values.size
|
|
80
|
-
end
|
|
81
|
-
count
|
|
73
|
+
issues.sum { |_type, values| values.size }
|
|
82
74
|
end
|
|
83
75
|
|
|
84
76
|
# The full set of unique line numbers across all issue types
|
|
85
77
|
#
|
|
86
78
|
# @return [Array<Integer>] the full set of unique offending line numbers for the hit
|
|
87
79
|
def line_numbers
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
80
|
+
issues.values.flatten.uniq.sort
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Generates a hash representation for JSON serialization
|
|
84
|
+
#
|
|
85
|
+
# @return [Hash] hit data with file, weight, and line details
|
|
86
|
+
def to_h
|
|
87
|
+
{
|
|
88
|
+
file: relative_path,
|
|
89
|
+
weight: weight,
|
|
90
|
+
lines: lines_summary
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def relative_path
|
|
97
|
+
pathname.to_s.delete_prefix("#{Dir.pwd}/")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def lines_summary
|
|
101
|
+
line_numbers.map do |line_num|
|
|
102
|
+
traces = lines[line_num.to_s] || []
|
|
103
|
+
{
|
|
104
|
+
line: line_num,
|
|
105
|
+
types: traces.map(&:type).uniq,
|
|
106
|
+
count: traces.size
|
|
107
|
+
}
|
|
91
108
|
end
|
|
92
|
-
line_numbers.uniq.sort
|
|
93
109
|
end
|
|
94
110
|
end
|
|
95
111
|
end
|
data/lib/minitest/heat/issue.rb
CHANGED
|
@@ -200,7 +200,9 @@ module Minitest
|
|
|
200
200
|
#
|
|
201
201
|
# @return [String] the first line of the exception message
|
|
202
202
|
def first_line_of_exception_message
|
|
203
|
-
|
|
203
|
+
return '' if message.nil? || message.empty?
|
|
204
|
+
|
|
205
|
+
text = message.split("\n")[0].to_s
|
|
204
206
|
|
|
205
207
|
text.size > exception_message_limit ? "#{text[0..exception_message_limit]}..." : text
|
|
206
208
|
end
|
|
@@ -208,6 +210,32 @@ module Minitest
|
|
|
208
210
|
def exception_message_limit
|
|
209
211
|
200
|
|
210
212
|
end
|
|
213
|
+
|
|
214
|
+
# Generates a hash representation for JSON serialization
|
|
215
|
+
#
|
|
216
|
+
# @return [Hash] issue data
|
|
217
|
+
def to_h
|
|
218
|
+
{
|
|
219
|
+
type: type,
|
|
220
|
+
test_class: test_class,
|
|
221
|
+
test_name: test_identifier,
|
|
222
|
+
execution_time: execution_time,
|
|
223
|
+
assertions: assertions,
|
|
224
|
+
message: message,
|
|
225
|
+
test_location: locations.test_definition&.to_h,
|
|
226
|
+
failure_location: failure_location_hash
|
|
227
|
+
}
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
private
|
|
231
|
+
|
|
232
|
+
def failure_location_hash
|
|
233
|
+
location = locations.most_relevant
|
|
234
|
+
return nil if location.nil?
|
|
235
|
+
return nil if location == locations.test_definition
|
|
236
|
+
|
|
237
|
+
location.to_h
|
|
238
|
+
end
|
|
211
239
|
end
|
|
212
240
|
end
|
|
213
241
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
3
5
|
module Minitest
|
|
4
6
|
module Heat
|
|
5
7
|
# Consistent structure for extracting information about a given location. In addition to the
|
|
@@ -42,6 +44,17 @@ module Minitest
|
|
|
42
44
|
]
|
|
43
45
|
end
|
|
44
46
|
|
|
47
|
+
# Generates a hash representation for JSON serialization
|
|
48
|
+
#
|
|
49
|
+
# @return [Hash] location data with file, line, and container
|
|
50
|
+
def to_h
|
|
51
|
+
{
|
|
52
|
+
file: relative_filename,
|
|
53
|
+
line: line_number,
|
|
54
|
+
container: container
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
45
58
|
# A short relative pathname and line number pair
|
|
46
59
|
#
|
|
47
60
|
# @return [String] the short filename/line number combo. ex. `dir/file.rb:23`
|
data/lib/minitest/heat/map.rb
CHANGED
|
@@ -30,6 +30,13 @@ module Minitest
|
|
|
30
30
|
hot_files.take(MAXIMUM_FILES_TO_SHOW)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
# Generates a hash representation for JSON serialization
|
|
34
|
+
#
|
|
35
|
+
# @return [Array<Hash>] array of hit hashes sorted by weight (highest first)
|
|
36
|
+
def to_h
|
|
37
|
+
hot_files.map(&:to_h)
|
|
38
|
+
end
|
|
39
|
+
|
|
33
40
|
private
|
|
34
41
|
|
|
35
42
|
# Sorts the files by hit "weight" so that the most problematic files are at the beginning
|
|
@@ -121,7 +121,8 @@ module Minitest
|
|
|
121
121
|
def source_code_line_token(location)
|
|
122
122
|
return nil unless location.project_file?
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
source_line = location.source_code.line
|
|
125
|
+
[:muted, " #{Output::SYMBOLS[:arrow]} `#{source_line&.strip || '(source unavailable)'}`"]
|
|
125
126
|
end
|
|
126
127
|
|
|
127
128
|
def containining_element_token(location)
|
|
@@ -69,7 +69,9 @@ module Minitest
|
|
|
69
69
|
# @return [String] the cleaned up version of the test name
|
|
70
70
|
def test_name(issue)
|
|
71
71
|
test_prefix = 'test_'
|
|
72
|
-
identifier = issue.test_identifier
|
|
72
|
+
identifier = issue.test_identifier.to_s
|
|
73
|
+
|
|
74
|
+
return 'Unknown test' if identifier.empty?
|
|
73
75
|
|
|
74
76
|
if identifier.start_with?(test_prefix)
|
|
75
77
|
identifier.delete_prefix(test_prefix).gsub('_', ' ').capitalize
|
|
@@ -87,22 +89,24 @@ module Minitest
|
|
|
87
89
|
end
|
|
88
90
|
|
|
89
91
|
def test_location_tokens
|
|
92
|
+
source_line = locations.test_failure.source_code.line
|
|
90
93
|
[
|
|
91
94
|
[:default, locations.test_definition.relative_filename],
|
|
92
95
|
[:muted, ':'],
|
|
93
96
|
[:default, locations.test_definition.line_number],
|
|
94
97
|
arrow_token,
|
|
95
98
|
[:default, locations.test_failure.line_number],
|
|
96
|
-
[:muted, "\n #{
|
|
99
|
+
[:muted, "\n #{source_line&.strip || '(source unavailable)'}"]
|
|
97
100
|
]
|
|
98
101
|
end
|
|
99
102
|
|
|
100
103
|
def location_tokens
|
|
104
|
+
source_line = locations.most_relevant.source_code.line
|
|
101
105
|
[
|
|
102
106
|
[:default, locations.most_relevant.relative_filename],
|
|
103
107
|
[:muted, ':'],
|
|
104
108
|
[:default, locations.most_relevant.line_number],
|
|
105
|
-
[:muted, "\n #{
|
|
109
|
+
[:muted, "\n #{source_line&.strip || '(source unavailable)'}"]
|
|
106
110
|
]
|
|
107
111
|
end
|
|
108
112
|
|
|
@@ -110,12 +114,15 @@ module Minitest
|
|
|
110
114
|
filename = locations.project.filename
|
|
111
115
|
line_number = locations.project.line_number
|
|
112
116
|
source = Minitest::Heat::Source.new(filename, line_number: line_number)
|
|
117
|
+
source_line = source.line
|
|
113
118
|
|
|
114
|
-
[[:muted, " #{Output::SYMBOLS[:arrow]} `#{source
|
|
119
|
+
[[:muted, " #{Output::SYMBOLS[:arrow]} `#{source_line&.strip || '(source unavailable)'}`"]]
|
|
115
120
|
end
|
|
116
121
|
|
|
117
122
|
def summary_tokens
|
|
118
|
-
|
|
123
|
+
summary_text = issue.summary.to_s
|
|
124
|
+
cleaned = summary_text.delete_suffix('---------------').strip
|
|
125
|
+
[[:italicized, cleaned.empty? ? '(no details available)' : cleaned]]
|
|
119
126
|
end
|
|
120
127
|
|
|
121
128
|
def slowness_summary_tokens
|
|
@@ -9,7 +9,12 @@ module Minitest
|
|
|
9
9
|
|
|
10
10
|
attr_accessor :results, :timer
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
# Explicitly define issues to avoid Forwardable warning about Object#issues private method
|
|
13
|
+
def issues
|
|
14
|
+
@results.issues
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def_delegators :@results, :errors, :brokens, :failures, :skips, :painfuls, :slows, :problems?
|
|
13
18
|
|
|
14
19
|
def initialize(results, timer)
|
|
15
20
|
@results = results
|
|
@@ -17,10 +17,10 @@ module Minitest
|
|
|
17
17
|
failure: %i[default red],
|
|
18
18
|
skipped: %i[default yellow],
|
|
19
19
|
warning_light: %i[light yellow],
|
|
20
|
-
italicized: %i[italic
|
|
20
|
+
italicized: %i[italic default],
|
|
21
21
|
bold: %i[bold default],
|
|
22
22
|
default: %i[default default],
|
|
23
|
-
muted: %i[light
|
|
23
|
+
muted: %i[light default]
|
|
24
24
|
}.freeze
|
|
25
25
|
|
|
26
26
|
attr_accessor :style_key, :content
|
data/lib/minitest/heat/output.rb
CHANGED
|
@@ -154,8 +154,9 @@ module Minitest
|
|
|
154
154
|
tokens.each do |token|
|
|
155
155
|
begin
|
|
156
156
|
print Token.new(*token).to_s(token_format)
|
|
157
|
-
rescue
|
|
158
|
-
|
|
157
|
+
rescue ArgumentError => e
|
|
158
|
+
# Token format error - output debug info and continue
|
|
159
|
+
puts "Token error (#{e.message}): #{token.inspect}"
|
|
159
160
|
end
|
|
160
161
|
end
|
|
161
162
|
newline
|
|
@@ -35,15 +35,15 @@ module Minitest
|
|
|
35
35
|
# numbers if the tests reference a shared method with an assertion in it. So in those cases,
|
|
36
36
|
# the backtrace is simply the test definition
|
|
37
37
|
backtrace = if issue.error?
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
# With errors, we have a backtrace
|
|
39
|
+
issue.locations.backtrace.project_locations
|
|
40
|
+
else
|
|
41
|
+
# With failures, the test definition is the most granular backtrace equivalent
|
|
42
|
+
location = issue.locations.test_definition
|
|
43
|
+
location.raw_container = issue.test_identifier
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
[location]
|
|
46
|
+
end
|
|
47
47
|
|
|
48
48
|
@heat_map.add(pathname, line_number, issue.type, backtrace: backtrace)
|
|
49
49
|
end
|
|
@@ -76,6 +76,39 @@ module Minitest
|
|
|
76
76
|
@slows ||= select_issues(:slow).sort_by(&:execution_time).reverse
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
# Returns count statistics by issue type
|
|
80
|
+
#
|
|
81
|
+
# @return [Hash] counts for each issue type
|
|
82
|
+
def statistics
|
|
83
|
+
{
|
|
84
|
+
total: issues.size,
|
|
85
|
+
errors: errors.size,
|
|
86
|
+
broken: brokens.size,
|
|
87
|
+
failures: failures.size,
|
|
88
|
+
skipped: skips.size,
|
|
89
|
+
painful: painfuls.size,
|
|
90
|
+
slow: slows.size
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Returns all issues that are not successes
|
|
95
|
+
#
|
|
96
|
+
# @return [Array<Issue>] issues that represent problems (errors, failures, skips, slow)
|
|
97
|
+
def issues_with_problems
|
|
98
|
+
issues.select(&:hit?)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Generates a hash representation for JSON serialization
|
|
102
|
+
#
|
|
103
|
+
# @return [Hash] results data
|
|
104
|
+
def to_h
|
|
105
|
+
{
|
|
106
|
+
statistics: statistics,
|
|
107
|
+
heat_map: heat_map.to_h,
|
|
108
|
+
issues: issues_with_problems.map(&:to_h)
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
79
112
|
private
|
|
80
113
|
|
|
81
114
|
def select_issues(issue_type)
|