minitest-heat 0.0.6 → 0.0.10
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/.rubocop.yml +1 -0
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/lib/minitest/heat/backtrace/line.rb +118 -0
- data/lib/minitest/heat/backtrace.rb +48 -14
- data/lib/minitest/heat/hit.rb +28 -27
- data/lib/minitest/heat/issue.rb +92 -73
- data/lib/minitest/heat/location.rb +71 -53
- data/lib/minitest/heat/map.rb +14 -17
- data/lib/minitest/heat/output/backtrace.rb +28 -13
- data/lib/minitest/heat/output/issue.rb +93 -23
- data/lib/minitest/heat/output/map.rb +47 -24
- data/lib/minitest/heat/output/marker.rb +5 -3
- data/lib/minitest/heat/output/results.rb +32 -21
- data/lib/minitest/heat/output/source_code.rb +1 -1
- data/lib/minitest/heat/output/token.rb +2 -1
- data/lib/minitest/heat/output.rb +70 -2
- data/lib/minitest/heat/results.rb +27 -81
- data/lib/minitest/heat/timer.rb +81 -0
- data/lib/minitest/heat/version.rb +1 -1
- data/lib/minitest/heat.rb +3 -2
- data/lib/minitest/heat_plugin.rb +9 -17
- data/lib/minitest/heat_reporter.rb +29 -23
- metadata +4 -3
- data/lib/minitest/heat/line.rb +0 -74
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6c60c717971a6a1858de4337247b8bfd37f63a63f31a1783de4ecaaad8ca5a8c
|
4
|
+
data.tar.gz: c491cb526bd8a243810fe88da7d15679a6c5182b256a5187db4c457bcc9b7a7a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: abf3a4c476c1041f75fedfa3eac1235f134bc99bbbf1c909e6e407e461380e9028079e3ad927180d7bec4f039c54247172c9a12bee579dec5eab53b444cf3f68
|
7
|
+
data.tar.gz: fb7d2373153fdaaf6fe8b3a6ec693baf3e04a8fa8dc5636f75d1b0afb45e5a7b25365f3dbe44b0aa3c53bb79b8ed7b0551c3798ceaa3477ce1d9e122159e116a
|
data/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Minitest
|
6
|
+
module Heat
|
7
|
+
class Backtrace
|
8
|
+
# Represents a line from a backtrace to provide more convenient access to information about
|
9
|
+
# the relevant file and line number for displaying in test results
|
10
|
+
class Line
|
11
|
+
attr_accessor :pathname, :number, :container
|
12
|
+
alias line_number number
|
13
|
+
|
14
|
+
# Creates an instance of a line number reference
|
15
|
+
# @param pathname: [Pathname, String] the full pathname to the file
|
16
|
+
# @param number: [Integer, String] the line number in question
|
17
|
+
# @param container: nil [String] the containing method or block for the line of code
|
18
|
+
#
|
19
|
+
# @return [self]
|
20
|
+
def initialize(pathname:, number:, container: nil)
|
21
|
+
@pathname = Pathname(pathname)
|
22
|
+
@number = number.to_i
|
23
|
+
@container = container.to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
# Parses a line from a backtrace in order to convert it to usable components
|
27
|
+
def self.parse_backtrace(raw_text)
|
28
|
+
raw_pathname, raw_line_number, raw_container = raw_text.split(':')
|
29
|
+
raw_container = raw_container.delete_prefix('in `').delete_suffix("'")
|
30
|
+
|
31
|
+
new(pathname: raw_pathname, number: raw_line_number, container: raw_container)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Generates a formatted string describing the line of code similar to the original backtrace
|
35
|
+
#
|
36
|
+
# @return [String] a consistently-formatted, human-readable string about the line of code
|
37
|
+
def to_s
|
38
|
+
"#{location} in `#{container}`"
|
39
|
+
end
|
40
|
+
|
41
|
+
# A safe interface to getting a string representing the path portion of the file
|
42
|
+
#
|
43
|
+
# @return [String] either the path/directory portion of the file name or '(Unrecognized File)'
|
44
|
+
# if the offending file can't be found for some reason
|
45
|
+
def path
|
46
|
+
pathname.exist? ? pathname.dirname : UNRECOGNIZED
|
47
|
+
end
|
48
|
+
|
49
|
+
# A safe interface for getting a string representing the filename portion of the file
|
50
|
+
#
|
51
|
+
# @return [String] either the filename portion of the file or '(Unrecognized File)'
|
52
|
+
# if the offending file can't be found for some reason
|
53
|
+
def file
|
54
|
+
pathname.exist? ? pathname.basename : UNRECOGNIZED
|
55
|
+
end
|
56
|
+
|
57
|
+
# A safe interface to getting the last modified time for the file in question
|
58
|
+
#
|
59
|
+
# @return [Time] the timestamp for when the file in question was last modified or `Time.at(0)`
|
60
|
+
# if the offending file can't be found for some reason
|
61
|
+
def mtime
|
62
|
+
pathname.exist? ? pathname.mtime : UNKNOWN_MODIFICATION_TIME
|
63
|
+
end
|
64
|
+
|
65
|
+
# A safe interface to getting the number of seconds since the file was modified
|
66
|
+
#
|
67
|
+
# @return [Integer] the number of seconds since the file was modified or `-1` if the offending
|
68
|
+
# file can't be found for some reason
|
69
|
+
def age_in_seconds
|
70
|
+
pathname.exist? ? seconds_ago : UNKNOWN_MODIFICATION_SECONDS
|
71
|
+
end
|
72
|
+
|
73
|
+
# A convenient method for getting the full location identifier using the full pathname and
|
74
|
+
# line number separated by a `:`
|
75
|
+
#
|
76
|
+
# @return [String] the full pathname and line number
|
77
|
+
def location
|
78
|
+
"#{pathname}:#{number}"
|
79
|
+
end
|
80
|
+
|
81
|
+
# A convenient method for getting the short location with `Dir.pwd` removed
|
82
|
+
#
|
83
|
+
# @return [String] the relative pathname and line number
|
84
|
+
def short_location
|
85
|
+
"#{file}:#{number}"
|
86
|
+
end
|
87
|
+
|
88
|
+
# A convenient method for getting the line of source code for the offending line number
|
89
|
+
#
|
90
|
+
# @return [String] the source code for the file/line number combination
|
91
|
+
def source_code(max_line_count: 1)
|
92
|
+
Minitest::Heat::Source.new(
|
93
|
+
pathname.to_s,
|
94
|
+
line_number: line_number,
|
95
|
+
max_line_count: max_line_count
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Determines if a given file follows the standard approaching to naming test files.
|
100
|
+
#
|
101
|
+
# @return [Boolean] true if the file name starts with `test_` or ends with `_test.rb`
|
102
|
+
def test_file?
|
103
|
+
file.to_s.start_with?('test_') || file.to_s.end_with?('_test.rb')
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
UNRECOGNIZED = '(Unrecognized File)'
|
109
|
+
UNKNOWN_MODIFICATION_TIME = Time.at(0)
|
110
|
+
UNKNOWN_MODIFICATION_SECONDS = -1
|
111
|
+
|
112
|
+
def seconds_ago
|
113
|
+
(Time.now - mtime).to_i
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -1,69 +1,103 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'backtrace/line'
|
4
|
+
|
3
5
|
module Minitest
|
4
6
|
module Heat
|
5
7
|
# Wrapper for separating backtrace into component parts
|
6
8
|
class Backtrace
|
7
9
|
attr_reader :raw_backtrace
|
8
10
|
|
11
|
+
# Creates a more flexible backtrace data structure by parsing the lines of the backtrace to
|
12
|
+
# extract individual elements for investigating the offending files and line numbers
|
13
|
+
# @param raw_backtrace [Array] the array of lines from the backtrace
|
14
|
+
#
|
15
|
+
# @return [self]
|
9
16
|
def initialize(raw_backtrace)
|
10
|
-
@raw_backtrace = raw_backtrace
|
17
|
+
@raw_backtrace = Array(raw_backtrace)
|
11
18
|
end
|
12
19
|
|
20
|
+
# Determines if the raw backtrace has values in it
|
21
|
+
#
|
22
|
+
# @return [Boolean] true if there's no backtrace or it's empty
|
13
23
|
def empty?
|
14
|
-
raw_backtrace.
|
24
|
+
raw_backtrace.empty?
|
15
25
|
end
|
16
26
|
|
27
|
+
# The final location exposed in the backtrace. Could be a line from the project or from a
|
28
|
+
# dependency or the Ruby core libraries
|
29
|
+
#
|
30
|
+
# @return [Line] the final location from the backtrace parsed as a Backtrace::Line
|
17
31
|
def final_location
|
18
32
|
parsed_entries.first
|
19
33
|
end
|
20
34
|
|
35
|
+
# The final location from within the project exposed in the backtrace. Could be test files or
|
36
|
+
# source code files
|
37
|
+
#
|
38
|
+
# @return [Line] the final project location from the backtrace parsed as a Backtrace::Line
|
21
39
|
def final_project_location
|
22
40
|
project_entries.first
|
23
41
|
end
|
24
42
|
|
43
|
+
# The most recently modified location from within the project
|
44
|
+
#
|
45
|
+
# @return [Line] the most recently modified project location from the backtrace parsed as a
|
46
|
+
# Backtrace::Line
|
25
47
|
def freshest_project_location
|
26
48
|
recently_modified_entries.first
|
27
49
|
end
|
28
50
|
|
51
|
+
# The final location from within the project source code (i.e. excluding tests)
|
52
|
+
#
|
53
|
+
# @return [Line] the final source code location from the backtrace parsed as a Backtrace::Line
|
29
54
|
def final_source_code_location
|
30
55
|
source_code_entries.first
|
31
56
|
end
|
32
57
|
|
58
|
+
# The final location from within the project's tests (i.e. excluding source code)
|
59
|
+
#
|
60
|
+
# @return [Line] the final test location from the backtrace parsed as a Backtrace::Line
|
33
61
|
def final_test_location
|
34
62
|
test_entries.first
|
35
63
|
end
|
36
64
|
|
65
|
+
# All entries from the backtrace that are files within the project
|
66
|
+
#
|
67
|
+
# @return [Line] the backtrace lines from within the project parsed as Backtrace::Line's
|
37
68
|
def project_entries
|
38
69
|
@project_entries ||= parsed_entries.select { |entry| entry.path.to_s.include?(Dir.pwd) }
|
39
70
|
end
|
40
71
|
|
72
|
+
# All entries from the backtrace within the project and sorted with the most recently modified
|
73
|
+
# files at the beginning
|
74
|
+
#
|
75
|
+
# @return [Line] the sorted backtrace lines from the project parsed as Backtrace::Line's
|
41
76
|
def recently_modified_entries
|
42
77
|
@recently_modified_entries ||= project_entries.sort_by(&:mtime).reverse
|
43
78
|
end
|
44
79
|
|
80
|
+
# All entries from the backtrace within the project tests
|
81
|
+
#
|
82
|
+
# @return [Line] the backtrace lines from within the project tests parsed as Backtrace::Line's
|
45
83
|
def test_entries
|
46
|
-
@
|
84
|
+
@test_entries ||= project_entries.select(&:test_file?)
|
47
85
|
end
|
48
86
|
|
87
|
+
# All source code entries from the backtrace (i.e. excluding tests)
|
88
|
+
#
|
89
|
+
# @return [Line] the backtrace lines from within the source code parsed as Backtrace::Line's
|
49
90
|
def source_code_entries
|
50
91
|
@source_code_entries ||= project_entries - test_entries
|
51
92
|
end
|
52
93
|
|
94
|
+
# All lines of the backtrace converted to Backtrace::Line's
|
95
|
+
#
|
96
|
+
# @return [Line] the full set of backtrace lines parsed as Backtrace::Line instances
|
53
97
|
def parsed_entries
|
54
98
|
return [] if raw_backtrace.nil?
|
55
99
|
|
56
|
-
@parsed_entries ||= raw_backtrace.map { |entry| Line.parse_backtrace(entry) }
|
57
|
-
end
|
58
|
-
|
59
|
-
private
|
60
|
-
|
61
|
-
def parse(entry)
|
62
|
-
Line.parse_backtrace(entry)
|
63
|
-
end
|
64
|
-
|
65
|
-
def test_file?(entry)
|
66
|
-
entry.file.to_s.end_with?('_test.rb') || entry.file.to_s.start_with?('test_')
|
100
|
+
@parsed_entries ||= raw_backtrace.map { |entry| Backtrace::Line.parse_backtrace(entry) }
|
67
101
|
end
|
68
102
|
end
|
69
103
|
end
|
data/lib/minitest/heat/hit.rb
CHANGED
@@ -5,7 +5,7 @@ require 'forwardable'
|
|
5
5
|
module Minitest
|
6
6
|
module Heat
|
7
7
|
# Kind of like an issue, but instead of focusing on a failing test, it covers all issues for a
|
8
|
-
# given file
|
8
|
+
# given file to build a heat map of the affected files
|
9
9
|
class Hit
|
10
10
|
# So we can sort hot spots by liklihood of being the most important spot to check out before
|
11
11
|
# trying to fix something. These are ranked based on the possibility they represent ripple
|
@@ -15,46 +15,41 @@ module Minitest
|
|
15
15
|
# test is broken (i.e. raising an exception), that's a special sort of failure that would be
|
16
16
|
# misleading. It doesn't represent a proper failure, but rather a test that doesn't work.
|
17
17
|
WEIGHTS = {
|
18
|
-
error:
|
19
|
-
broken:
|
20
|
-
failure:
|
21
|
-
skipped:
|
22
|
-
painful:
|
18
|
+
error: 5, # exceptions from source code have the highest likelihood of a ripple effect
|
19
|
+
broken: 4, # broken tests won't have ripple effects but can't help if they can't run
|
20
|
+
failure: 3, # failures are kind of the whole point, and they could have ripple effects
|
21
|
+
skipped: 2, # skips aren't failures, but they shouldn't go ignored
|
22
|
+
painful: 1, # slow tests aren't failures, but they shouldn't be ignored
|
23
23
|
slow: 0
|
24
24
|
}.freeze
|
25
25
|
|
26
26
|
attr_reader :pathname, :issues
|
27
27
|
|
28
|
+
# Creates an instance of a Hit for the given pathname. It must be the full pathname to
|
29
|
+
# uniquely identify the file or we could run into collisions that muddy the water and
|
30
|
+
# obscure which files had which errors on which line numbers
|
31
|
+
# @param pathname [Pathname,String] the full pathname to the file
|
32
|
+
#
|
33
|
+
# @return [self]
|
28
34
|
def initialize(pathname)
|
29
35
|
@pathname = Pathname(pathname)
|
30
36
|
@issues = {}
|
31
37
|
end
|
32
38
|
|
39
|
+
# Adds a record of a given issue type for the line number
|
40
|
+
# @param type [Symbol] one of Issue::TYPES
|
41
|
+
# @param line_number [Integer,String] the line number to record the issue on
|
42
|
+
#
|
43
|
+
# @return [type] [description]
|
33
44
|
def log(type, line_number)
|
34
45
|
@issues[type] ||= []
|
35
|
-
@issues[type] << line_number
|
36
|
-
end
|
37
|
-
|
38
|
-
def mtime
|
39
|
-
pathname.mtime
|
40
|
-
end
|
41
|
-
|
42
|
-
def age_in_seconds
|
43
|
-
(Time.now - mtime).to_i
|
44
|
-
end
|
45
|
-
|
46
|
-
def critical_issues?
|
47
|
-
issues[:error].any? || issues[:broken].any? || issues[:failure].any?
|
48
|
-
end
|
49
|
-
|
50
|
-
def issue_count
|
51
|
-
count = 0
|
52
|
-
Issue::TYPES.each do |issue_type|
|
53
|
-
count += issues.fetch(issue_type) { [] }.size
|
54
|
-
end
|
55
|
-
count
|
46
|
+
@issues[type] << Integer(line_number)
|
56
47
|
end
|
57
48
|
|
49
|
+
# Calcuates an approximate weight to serve as a proxy for which files are most likely to be
|
50
|
+
# the most problematic across the various issue types
|
51
|
+
#
|
52
|
+
# @return [Integer] the problem weight for the file
|
58
53
|
def weight
|
59
54
|
weight = 0
|
60
55
|
issues.each_pair do |type, values|
|
@@ -63,6 +58,9 @@ module Minitest
|
|
63
58
|
weight
|
64
59
|
end
|
65
60
|
|
61
|
+
# The total issue count for the file across all issue types. Includes duplicates if they exist
|
62
|
+
#
|
63
|
+
# @return [Integer] the sum of the counts for all line numbers for all issue types
|
66
64
|
def count
|
67
65
|
count = 0
|
68
66
|
issues.each_pair do |_type, values|
|
@@ -71,6 +69,9 @@ module Minitest
|
|
71
69
|
count
|
72
70
|
end
|
73
71
|
|
72
|
+
# The full set of unique line numbers across all issue types
|
73
|
+
#
|
74
|
+
# @return [Array<Integer>] the full set of unique offending line numbers for the hit
|
74
75
|
def line_numbers
|
75
76
|
line_numbers = []
|
76
77
|
issues.each_pair do |_type, values|
|
data/lib/minitest/heat/issue.rb
CHANGED
@@ -17,36 +17,68 @@ module Minitest
|
|
17
17
|
painful: 3.0
|
18
18
|
}.freeze
|
19
19
|
|
20
|
-
attr_reader :
|
21
|
-
|
22
|
-
|
23
|
-
|
20
|
+
attr_reader :assertions,
|
21
|
+
:location,
|
22
|
+
:message,
|
23
|
+
:test_class,
|
24
|
+
:test_identifier,
|
25
|
+
:execution_time,
|
26
|
+
:passed,
|
27
|
+
:error,
|
28
|
+
:skipped
|
29
|
+
|
30
|
+
def_delegators :@location, :backtrace, :test_definition_line, :test_failure_line
|
31
|
+
|
32
|
+
# Extracts the necessary data from result.
|
33
|
+
# @param result [Minitest::Result] the instance of Minitest::Result to examine
|
34
|
+
#
|
35
|
+
# @return [Issue] the instance of the issue to use for examining the result
|
36
|
+
def self.from_result(result)
|
37
|
+
# Not all results are failures, so we use the safe navigation operator
|
38
|
+
exception = result.failure&.exception
|
39
|
+
|
40
|
+
new(
|
41
|
+
assertions: result.assertions,
|
42
|
+
location: result.source_location,
|
43
|
+
test_class: result.klass,
|
44
|
+
test_identifier: result.name,
|
45
|
+
execution_time: result.time,
|
46
|
+
passed: result.passed?,
|
47
|
+
error: result.error?,
|
48
|
+
skipped: result.skipped?,
|
49
|
+
message: exception&.message,
|
50
|
+
backtrace: exception&.backtrace,
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Creates an instance of Issue. In general, the `from_result` approach will be more convenient
|
55
|
+
# for standard usage, but for lower-level purposes like testing, the initializer provides3
|
56
|
+
# more fine-grained control
|
57
|
+
# @param assertions: 1 [Integer] the number of assertions in the result
|
58
|
+
# @param message: nil [String] exception if there is one
|
59
|
+
# @param backtrace: [] [Array<String>] the array of backtrace lines from an exception
|
60
|
+
# @param location: nil [String] the location identifier for a test
|
61
|
+
# @param test_class: nil [String] the class name for the test result's containing class
|
62
|
+
# @param test_identifier: nil [String] the name of the test
|
63
|
+
# @param execution_time: nil [Float] the time it took to run the test
|
64
|
+
# @param passed: false [Boolean] true if the test explicitly passed, false otherwise
|
65
|
+
# @param error: false [Boolean] true if the test raised an exception
|
66
|
+
# @param skipped: false [Boolean] true if the test was skipped
|
67
|
+
#
|
68
|
+
# @return [type] [description]
|
69
|
+
def initialize(assertions: 1, location: ['unknown', 1], backtrace: [], execution_time: 0.0, message: nil, test_class: nil, test_identifier: nil, passed: false, error: false, skipped: false)
|
70
|
+
@message = message
|
24
71
|
|
25
|
-
|
26
|
-
@
|
72
|
+
@assertions = Integer(assertions)
|
73
|
+
@location = Location.new(location, backtrace)
|
27
74
|
|
28
|
-
@
|
29
|
-
@
|
30
|
-
|
31
|
-
|
32
|
-
# Returns the primary location of the issue with the present working directory removed from
|
33
|
-
# the string for conciseness
|
34
|
-
#
|
35
|
-
# @return [String] the pathname for the file relative to the present working directory
|
36
|
-
def short_location
|
37
|
-
location.to_s.delete_prefix(Dir.pwd)
|
38
|
-
end
|
75
|
+
@test_class = test_class
|
76
|
+
@test_identifier = test_identifier
|
77
|
+
@execution_time = Float(execution_time)
|
39
78
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
# support generating the heat map
|
44
|
-
def to_hit
|
45
|
-
[
|
46
|
-
location.project_file.to_s,
|
47
|
-
location.project_failure_line,
|
48
|
-
type
|
49
|
-
]
|
79
|
+
@passed = passed
|
80
|
+
@error = error
|
81
|
+
@skipped = skipped
|
50
82
|
end
|
51
83
|
|
52
84
|
# Classifies different issue types so they can be categorized, organized, and prioritized.
|
@@ -58,7 +90,7 @@ module Minitest
|
|
58
90
|
# painfully slow and should get more attention.
|
59
91
|
#
|
60
92
|
# @return [Symbol] issue type for classifying issues and reporting
|
61
|
-
def type
|
93
|
+
def type # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
62
94
|
if error? && in_test?
|
63
95
|
:broken
|
64
96
|
elsif error?
|
@@ -81,7 +113,7 @@ module Minitest
|
|
81
113
|
#
|
82
114
|
# @return [Boolean] true if the test did not pass or if it was slow
|
83
115
|
def hit?
|
84
|
-
!passed? || slow?
|
116
|
+
!passed? || slow? || painful?
|
85
117
|
end
|
86
118
|
|
87
119
|
# Determines if a test should be considered slow by comparing it to the low end definition of
|
@@ -89,7 +121,7 @@ module Minitest
|
|
89
121
|
#
|
90
122
|
# @return [Boolean] true if the test took longer to run than `SLOW_THRESHOLDS[:slow]`
|
91
123
|
def slow?
|
92
|
-
|
124
|
+
execution_time >= SLOW_THRESHOLDS[:slow] && execution_time < SLOW_THRESHOLDS[:painful]
|
93
125
|
end
|
94
126
|
|
95
127
|
# Determines if a test should be considered painfully slow by comparing it to the high end
|
@@ -97,7 +129,7 @@ module Minitest
|
|
97
129
|
#
|
98
130
|
# @return [Boolean] true if the test took longer to run than `SLOW_THRESHOLDS[:painful]`
|
99
131
|
def painful?
|
100
|
-
|
132
|
+
execution_time >= SLOW_THRESHOLDS[:painful]
|
101
133
|
end
|
102
134
|
|
103
135
|
# Determines if the issue is an exception that was raised from directly within a test
|
@@ -117,57 +149,44 @@ module Minitest
|
|
117
149
|
location.proper_failure?
|
118
150
|
end
|
119
151
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
def
|
125
|
-
|
126
|
-
end
|
127
|
-
|
128
|
-
def test_name
|
129
|
-
test_identifier.delete_prefix('test_').gsub('_', ' ').capitalize
|
130
|
-
end
|
131
|
-
|
132
|
-
def exception
|
133
|
-
failure.exception
|
134
|
-
end
|
135
|
-
|
136
|
-
def time
|
137
|
-
result.time
|
152
|
+
# Was the result a pass? i.e. Skips aren't passes or failures. Slows are still passes. So this
|
153
|
+
# is purely a measure of whether the test explicitly passed all assertions
|
154
|
+
#
|
155
|
+
# @return [Boolean] false for errors, failures, or skips, true for passes (including slows)
|
156
|
+
def passed?
|
157
|
+
passed
|
138
158
|
end
|
139
159
|
|
140
|
-
|
141
|
-
|
160
|
+
# Was there an exception that triggered a failure?
|
161
|
+
#
|
162
|
+
# @return [Boolean] true if there's an exception
|
163
|
+
def error?
|
164
|
+
error
|
142
165
|
end
|
143
166
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
'Broken Test'
|
150
|
-
elsif error? || !passed?
|
151
|
-
failure.result_label
|
152
|
-
elsif painful?
|
153
|
-
'Passed but Very Slow'
|
154
|
-
elsif slow?
|
155
|
-
'Passed but Slow'
|
156
|
-
end
|
167
|
+
# Was the test skipped?
|
168
|
+
#
|
169
|
+
# @return [Boolean] true if the test was explicitly skipped, false otherwise
|
170
|
+
def skipped?
|
171
|
+
skipped
|
157
172
|
end
|
158
173
|
|
174
|
+
# The more nuanced detail of the failure. If it's an error, digs into the exception. Otherwise
|
175
|
+
# uses the message from the result
|
176
|
+
#
|
177
|
+
# @return [String] a more detailed explanation of the issue
|
159
178
|
def summary
|
160
|
-
|
179
|
+
# When there's an exception, use the first line from the exception message. Otherwise, the
|
180
|
+
# message represents explanation for a test failure, and should be used in full
|
181
|
+
error? ? first_line_of_exception_message : message
|
161
182
|
end
|
162
183
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
def exception_parts
|
170
|
-
exception.message.split("\n")
|
184
|
+
# Returns the first line of an exception message when the issue is from a proper exception
|
185
|
+
# failure since exception messages can be long and cumbersome.
|
186
|
+
#
|
187
|
+
# @return [String] the first line of the exception message
|
188
|
+
def first_line_of_exception_message
|
189
|
+
message.split("\n")[0]
|
171
190
|
end
|
172
191
|
end
|
173
192
|
end
|