minitest-heat 0.0.6 → 0.0.10
Sign up to get free protection for your applications and to get access to all the features.
- 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
|