what_weve_got_here_is_an_error_to_communicate 0.0.6 → 0.0.7
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 +5 -13
- data/lib/error_to_communicate/at_exit.rb +12 -2
- data/lib/error_to_communicate/config.rb +6 -6
- data/lib/error_to_communicate/exception_info.rb +10 -7
- data/lib/error_to_communicate/format_terminal.rb +6 -1
- data/lib/error_to_communicate/heuristic/no_method_error.rb +51 -0
- data/lib/error_to_communicate/heuristic/syntax_error.rb +2 -1
- data/lib/error_to_communicate/levenshtein.rb +34 -0
- data/lib/error_to_communicate/rspec_formatter.rb +34 -9
- data/lib/error_to_communicate/version.rb +1 -1
- data/spec/acceptance/unexpected_nil_spec.rb +44 -0
- data/spec/config_spec.rb +13 -8
- data/spec/heuristic/no_method_error_spec.rb +50 -0
- data/spec/heuristic/spec_helper.rb +2 -1
- data/spec/heuristic/wrong_number_of_arguments_spec.rb +1 -1
- data/spec/heuristic_spec.rb +1 -1
- data/spec/levenshtein_spec.rb +64 -0
- data/spec/parsing_exception_info_spec.rb +11 -6
- data/spec/rspec_formatter_spec.rb +35 -9
- data/spec/spec_helper.rb +2 -2
- data/what_weve_got_here_is_an_error_to_communicate.gemspec +2 -2
- metadata +55 -24
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
ODY5NzA5MDJiNWEzOWM2OGUwZmI1NDcwOTk0YjIxYTc2MzIyYjVkYw==
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f9ff0e4e8188c71874623d5b7f91c3b1628c733c
|
4
|
+
data.tar.gz: aa5acc32a411201dc00d52ebf551b533a36d85fc
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
10
|
-
MTU3ZGNkODY2NThhMmQ4Yzc2MmE4ODliODRmY2NiNTZhYjUwNDA5N2QxMGVl
|
11
|
-
NmM3YjUzNWZkMjk4ZWQ2MTA0NTA2OWJhMDYyNjg5MjU5NDc5YTA=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
YzQzZjBkZjhjNzgyNjEzM2JiN2RiZWQ4ZjU1ZGJiZjAzNTFlNTU1ZDUwNWZl
|
14
|
-
ZDRkYzc4YmEzYTcxMzViZWM2NGVkYjI3YTUxMWIyNjA5Y2JhNWYyYjg1NGVh
|
15
|
-
ZmEyYTlkZmIxODA5NzY1NTBhMGM2YWY0NGZhN2RhM2I0YTc3YTQ=
|
6
|
+
metadata.gz: ad7c22761d89cce4c92264aadd62b3c1c1609d5713ed97c76d06d50cb6c7e5057b8856a3ac2240cb0f14122401cc51c10453bff5ff1f3385a1292cc1124205af
|
7
|
+
data.tar.gz: 67ced68e105811bb22d0a1382eced38ae2830e1c8ebcb06f62852aeb9c0bde436b654217b31c3641452b2a674eee06c2eb45b2a63741a1c8461373cdada82fae
|
@@ -1,13 +1,23 @@
|
|
1
|
+
require 'interception'
|
1
2
|
require 'error_to_communicate'
|
2
3
|
|
4
|
+
error_binding = nil
|
5
|
+
recording_code = lambda { |_exc, binding|
|
6
|
+
error_binding = binding
|
7
|
+
}
|
8
|
+
|
9
|
+
Interception.listen &recording_code
|
10
|
+
|
3
11
|
# Deal with global deps and console knowledge here
|
4
12
|
at_exit do
|
13
|
+
Interception.unlisten recording_code
|
5
14
|
exception = $!
|
6
15
|
config = ErrorToCommunicate::Config.default
|
7
16
|
|
8
|
-
next unless config.accept? exception
|
17
|
+
next unless config.accept? exception, error_binding
|
18
|
+
|
19
|
+
heuristic = config.heuristic_for exception, error_binding
|
9
20
|
|
10
|
-
heuristic = config.heuristic_for exception
|
11
21
|
formatted = config.format heuristic, Dir.pwd
|
12
22
|
$stderr.puts formatted
|
13
23
|
|
@@ -50,15 +50,15 @@ module ErrorToCommunicate
|
|
50
50
|
self.project = Project.new root: root, loaded_features: loaded_features
|
51
51
|
end
|
52
52
|
|
53
|
-
def accept?(exception)
|
54
|
-
return false unless ExceptionInfo.parseable? exception
|
55
|
-
einfo = ExceptionInfo.parse(exception)
|
53
|
+
def accept?(exception, binding)
|
54
|
+
return false unless ExceptionInfo.parseable? exception, binding
|
55
|
+
einfo = ExceptionInfo.parse(exception, binding)
|
56
56
|
!blacklist.call(einfo) && !!heuristics.find { |h| h.for? einfo }
|
57
57
|
end
|
58
58
|
|
59
|
-
def heuristic_for(exception)
|
60
|
-
accept?(exception) || raise(ArgumentError, "Asked for a heuristic on an object we don't accept: #{exception.inspect}")
|
61
|
-
einfo = ExceptionInfo.parse(exception)
|
59
|
+
def heuristic_for(exception, binding)
|
60
|
+
accept?(exception, binding) || raise(ArgumentError, "Asked for a heuristic on an object we don't accept: #{exception.inspect}")
|
61
|
+
einfo = ExceptionInfo.parse(exception, binding)
|
62
62
|
heuristics.find { |heuristic| heuristic.for? einfo }
|
63
63
|
.new(einfo: einfo, project: project)
|
64
64
|
end
|
@@ -6,7 +6,7 @@ module ErrorToCommunicate
|
|
6
6
|
end
|
7
7
|
|
8
8
|
class ErrorToCommunicate::ExceptionInfo::Location
|
9
|
-
attr_accessor :path, :linenum, :label, :pred, :succ
|
9
|
+
attr_accessor :path, :linenum, :label, :pred, :succ, :binding
|
10
10
|
|
11
11
|
def initialize(attributes)
|
12
12
|
self.path = Pathname.new attributes.fetch(:path)
|
@@ -14,17 +14,19 @@ class ErrorToCommunicate::ExceptionInfo::Location
|
|
14
14
|
self.label = attributes.fetch :label
|
15
15
|
self.pred = attributes.fetch :pred, nil
|
16
16
|
self.succ = attributes.fetch :succ, nil
|
17
|
+
self.binding = attributes.fetch :binding
|
17
18
|
end
|
18
19
|
|
19
20
|
# What if the line doesn't match for some reason?
|
20
21
|
# Raise an exception?
|
21
22
|
# Use some reasonable default? (is there one?)
|
22
|
-
def self.parse(line)
|
23
|
+
def self.parse(line, binding)
|
23
24
|
line =~ /^(.*?):(\d+):in `(.*?)'$/ # Are ^ and $ sufficient? Should be \A and (\Z or \z)?
|
24
25
|
ErrorToCommunicate::ExceptionInfo::Location.new(
|
25
26
|
path: ($1||""),
|
26
27
|
linenum: ($2||"-1").to_i,
|
27
28
|
label: ($3||line),
|
29
|
+
binding: binding
|
28
30
|
)
|
29
31
|
end
|
30
32
|
|
@@ -55,6 +57,7 @@ class ErrorToCommunicate::ExceptionInfo
|
|
55
57
|
attr_accessor :classname
|
56
58
|
attr_accessor :message
|
57
59
|
attr_accessor :backtrace
|
60
|
+
attr_accessor :binding
|
58
61
|
|
59
62
|
def initialize(attributes)
|
60
63
|
self.exception = attributes.fetch :exception, nil
|
@@ -74,22 +77,22 @@ class ErrorToCommunicate::ExceptionInfo
|
|
74
77
|
@exception
|
75
78
|
end
|
76
79
|
|
77
|
-
def self.parseable?(exception)
|
80
|
+
def self.parseable?(exception, binding)
|
78
81
|
exception.respond_to?(:message) && exception.respond_to?(:backtrace)
|
79
82
|
end
|
80
83
|
|
81
|
-
def self.parse(exception)
|
84
|
+
def self.parse(exception, binding)
|
82
85
|
return exception if exception.kind_of? self
|
83
86
|
new exception: exception,
|
84
87
|
classname: exception.class.name,
|
85
88
|
message: exception.message,
|
86
|
-
backtrace: parse_backtrace(exception.backtrace)
|
89
|
+
backtrace: parse_backtrace(exception.backtrace, binding)
|
87
90
|
end
|
88
91
|
|
89
|
-
def self.parse_backtrace(backtrace)
|
92
|
+
def self.parse_backtrace(backtrace, binding)
|
90
93
|
# Really, there are better methods, e.g. backtrace_locations,
|
91
94
|
# but they're unevenly implemented across versions and implementations
|
92
|
-
backtrace = (backtrace||[]).map { |line| Location.parse line }
|
95
|
+
backtrace = (backtrace||[]).map { |line| Location.parse line, binding }
|
93
96
|
backtrace.each_cons(2) { |crnt, pred| crnt.pred, pred.succ = pred, crnt }
|
94
97
|
backtrace
|
95
98
|
end
|
@@ -38,7 +38,12 @@ module ErrorToCommunicate
|
|
38
38
|
when Array then semantic_content.map { |c| format c }.join
|
39
39
|
when :summary then format([:separator]) + format(content)
|
40
40
|
when :heuristic then format([:separator]) + format(content)
|
41
|
-
when :backtrace then
|
41
|
+
when :backtrace then
|
42
|
+
if content.any?
|
43
|
+
format([:separator]) + format(content)
|
44
|
+
else
|
45
|
+
format([:separator]) + format([:message, "No backtrace available"]) # TODO: Not tested, I hit this with capybara, when RSpec filtered everything out of the backtrace
|
46
|
+
end
|
42
47
|
when :separator then theme.separator_line
|
43
48
|
when :columns then theme.columns [content, *rest].map { |c| format c }
|
44
49
|
when :classname then theme.classname format content
|
@@ -1,4 +1,6 @@
|
|
1
|
+
require 'rouge'
|
1
2
|
require 'error_to_communicate/heuristic'
|
3
|
+
require 'error_to_communicate/levenshtein'
|
2
4
|
|
3
5
|
module ErrorToCommunicate
|
4
6
|
class Heuristic
|
@@ -25,8 +27,57 @@ module ErrorToCommunicate
|
|
25
27
|
]
|
26
28
|
end
|
27
29
|
|
30
|
+
def semantic_explanation
|
31
|
+
if misspelling?
|
32
|
+
"You called the method `#{undefined_method_name}' on `#{name_of_ivar}', which is nil\nPossible misspelling of `#{closest_name}'"
|
33
|
+
else
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
28
38
|
private
|
29
39
|
|
40
|
+
# FIXME:
|
41
|
+
# Needs to be able to deal with situations like
|
42
|
+
# the line number being within a multiline expression
|
43
|
+
def name_of_ivar
|
44
|
+
return @name_of_ivar if defined? @name_of_ivar
|
45
|
+
file = File.read(einfo.backtrace.first.path)
|
46
|
+
line = file.lines.to_a[einfo.backtrace.first.linenum-1]
|
47
|
+
|
48
|
+
tokens = Rouge::Lexers::Ruby.lex(line).to_a
|
49
|
+
index = tokens.index { |token, text| text == undefined_method_name }
|
50
|
+
|
51
|
+
while 0 <= index
|
52
|
+
token, text = tokens[index]
|
53
|
+
break if token.qualname == "Name.Variable.Instance"
|
54
|
+
index -= 1
|
55
|
+
end
|
56
|
+
|
57
|
+
@name_of_ivar = if index == -1
|
58
|
+
nil
|
59
|
+
else
|
60
|
+
token, text = tokens[index]
|
61
|
+
text
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def existing_ivars
|
66
|
+
# TODO: this should get pushed into the location
|
67
|
+
binding = einfo.backtrace[0].binding
|
68
|
+
return [] unless binding
|
69
|
+
binding.eval('self').instance_variables
|
70
|
+
end
|
71
|
+
|
72
|
+
def misspelling?
|
73
|
+
name_of_ivar && closest_name &&
|
74
|
+
Levenshtein.call(closest_name, name_of_ivar) <= 2
|
75
|
+
end
|
76
|
+
|
77
|
+
def closest_name
|
78
|
+
@closest_name ||= existing_ivars.min_by { |varname| Levenshtein.call varname, name_of_ivar }
|
79
|
+
end
|
80
|
+
|
30
81
|
def self.parse_undefined_name(message)
|
31
82
|
message[/`(.*)'/, 1]
|
32
83
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module ErrorToCommunicate
|
2
|
+
class Levenshtein
|
3
|
+
def self.call(target, actual)
|
4
|
+
new(target, actual).distance
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_accessor :target, :actual, :memoized
|
8
|
+
def initialize(target, actual)
|
9
|
+
self.target = target
|
10
|
+
self.actual = actual
|
11
|
+
self.memoized = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def distance
|
15
|
+
@distance ||= call target.length, actual.length
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(target_length, actual_length)
|
19
|
+
memoized[[target_length, actual_length]] ||=
|
20
|
+
if target_length.zero?
|
21
|
+
actual_length
|
22
|
+
elsif actual_length.zero?
|
23
|
+
target_length
|
24
|
+
elsif target[target_length - 1] == actual[actual_length - 1]
|
25
|
+
call(target_length - 1, actual_length - 1)
|
26
|
+
else
|
27
|
+
[ call(target_length , actual_length - 1),
|
28
|
+
call(target_length - 1, actual_length ),
|
29
|
+
call(target_length - 1, actual_length - 1),
|
30
|
+
].min + 1
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'interception'
|
1
2
|
require 'error_to_communicate'
|
2
3
|
require 'rspec/core/formatters/documentation_formatter'
|
3
4
|
|
@@ -17,10 +18,11 @@ module ErrorToCommunicate
|
|
17
18
|
self.failure_number = attributes.fetch :failure_number
|
18
19
|
self.failure = attributes.fetch :failure
|
19
20
|
self.config = attributes.fetch :config
|
21
|
+
binding = attributes.fetch :binding
|
20
22
|
|
21
23
|
# initialize the heuristic
|
22
|
-
ExceptionInfo.parse(failure.exception).tap do |einfo|
|
23
|
-
einfo.backtrace = ExceptionInfo.parse_backtrace failure.formatted_backtrace
|
24
|
+
ExceptionInfo.parse(failure.exception, binding).tap do |einfo|
|
25
|
+
einfo.backtrace = ExceptionInfo.parse_backtrace failure.formatted_backtrace, binding
|
24
26
|
super einfo: einfo, project: config.project
|
25
27
|
end
|
26
28
|
|
@@ -41,7 +43,7 @@ module ErrorToCommunicate
|
|
41
43
|
[:classname, failure.description]]]] # TODO: not classname
|
42
44
|
else
|
43
45
|
# wrap the heuristic that would otherwise be chosen
|
44
|
-
heuristic = config.heuristic_for einfo
|
46
|
+
heuristic = config.heuristic_for einfo, binding
|
45
47
|
self.semantic_info = heuristic.semantic_info
|
46
48
|
self.semantic_summary =
|
47
49
|
[:summary, [
|
@@ -60,8 +62,18 @@ module ErrorToCommunicate
|
|
60
62
|
end
|
61
63
|
end
|
62
64
|
|
65
|
+
module ExceptionRecorder
|
66
|
+
extend self
|
67
|
+
def record_exception_bindings(config)
|
68
|
+
config.around do |spec|
|
69
|
+
Thread.current[:e2c_last_binding_seen] = nil
|
70
|
+
Interception.listen(spec) { |_exc, binding| Thread.current[:e2c_last_binding_seen] ||= binding }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
::RSpec.configure { |config| record_exception_bindings config }
|
74
|
+
end
|
63
75
|
|
64
|
-
class RSpecFormatter < RSpec::Core::Formatters::DocumentationFormatter
|
76
|
+
class RSpecFormatter < ::RSpec::Core::Formatters::DocumentationFormatter
|
65
77
|
# Register for notifications from our parent classes
|
66
78
|
# http://rspec.info/documentation/3.2/rspec-core/RSpec/Core/Formatters.html
|
67
79
|
#
|
@@ -71,6 +83,22 @@ module ErrorToCommunicate
|
|
71
83
|
# }
|
72
84
|
RSpec::Core::Formatters.register self
|
73
85
|
|
86
|
+
def initialize(*)
|
87
|
+
@num_failures = 0
|
88
|
+
super
|
89
|
+
end
|
90
|
+
|
91
|
+
def example_failed(failure_notification)
|
92
|
+
super
|
93
|
+
# we must create it here, because it won't have access to the callstack later
|
94
|
+
example = failure_notification.example
|
95
|
+
example.metadata[:heuristic] = Heuristic::RSpecFailure.new \
|
96
|
+
config: Config.default, # E2C heuristic, not RSpec's
|
97
|
+
failure: failure_notification,
|
98
|
+
failure_number: (@num_failures += 1),
|
99
|
+
binding: Thread.current[:e2c_last_binding_seen]
|
100
|
+
end
|
101
|
+
|
74
102
|
# Use ErrorToCommunicate to print error info
|
75
103
|
# rather than default DocumentationFormatter.
|
76
104
|
#
|
@@ -82,11 +110,8 @@ module ErrorToCommunicate
|
|
82
110
|
# but we can't currently turn colour off in our output
|
83
111
|
def dump_failures(notification)
|
84
112
|
output.puts "\nFailures:\n"
|
85
|
-
notification.failure_notifications.each
|
86
|
-
heuristic =
|
87
|
-
config: Config.default,
|
88
|
-
failure: failure,
|
89
|
-
failure_number: failure_number
|
113
|
+
notification.failure_notifications.each do |notification|
|
114
|
+
heuristic = notification.example.metadata.fetch :heuristic
|
90
115
|
formatted = Config.default.format heuristic, Dir.pwd
|
91
116
|
output.puts formatted.chomp.gsub(/^/, ' ')
|
92
117
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'acceptance/spec_helper'
|
2
|
+
|
3
|
+
RSpec.context 'Exception', acceptance: true do
|
4
|
+
it 'Prints heuristics' do
|
5
|
+
write_file 'misspelled_ivar.rb', <<-BODY
|
6
|
+
require "what_weve_got_here_is_an_error_to_communicate"
|
7
|
+
User = Struct.new :name
|
8
|
+
|
9
|
+
class Email
|
10
|
+
def initialize(user)
|
11
|
+
@user = user
|
12
|
+
end
|
13
|
+
|
14
|
+
def body
|
15
|
+
"Dear, \#{@uesr.name}, <3 <3 <3"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
Email.new(User.new 'Jorge').body
|
20
|
+
BODY
|
21
|
+
|
22
|
+
invocation = ruby 'misspelled_ivar.rb'
|
23
|
+
stderr = strip_color invocation.stderr
|
24
|
+
|
25
|
+
# sanity
|
26
|
+
expect(invocation.stdout).to eq ''
|
27
|
+
expect(invocation.exitstatus).to eq 1
|
28
|
+
|
29
|
+
# error: It prints the exception class and prints the message
|
30
|
+
expect(stderr).to match /NoMethodError/
|
31
|
+
expect(stderr).to match /You called the method `name' on `@uesr', which is nil/
|
32
|
+
|
33
|
+
# Suggests a fix
|
34
|
+
expect(stderr).to match /possible misspelling of `@user'/i
|
35
|
+
|
36
|
+
# Shows where the method was called
|
37
|
+
expect(stderr).to include '"Dear, #{@uesr.name}, <3 <3 <3"'
|
38
|
+
|
39
|
+
# MAYBE: shows where the correctly spelled variable was set
|
40
|
+
|
41
|
+
# typical backtrace
|
42
|
+
expect(stderr).to include 'misspelled_ivar.rb:10'
|
43
|
+
end
|
44
|
+
end
|
data/spec/config_spec.rb
CHANGED
@@ -20,11 +20,13 @@ RSpec.describe 'configuration', config: true do
|
|
20
20
|
|
21
21
|
# helper methods
|
22
22
|
def yes_accept!(config, ex)
|
23
|
-
|
23
|
+
binding = nil
|
24
|
+
expect(config.accept? ex, binding).to eq true
|
24
25
|
end
|
25
26
|
|
26
27
|
def no_accept!(config, ex)
|
27
|
-
|
28
|
+
binding = nil
|
29
|
+
expect(config.accept? ex, binding).to eq false
|
28
30
|
end
|
29
31
|
|
30
32
|
def config_for(attrs)
|
@@ -72,18 +74,20 @@ RSpec.describe 'configuration', config: true do
|
|
72
74
|
end
|
73
75
|
|
74
76
|
describe 'finding the heuristic for an exception' do
|
75
|
-
it 'raises an ArgumentError if given an
|
77
|
+
it 'raises an ArgumentError if given an ecception that it won\'t accept' do
|
76
78
|
config = config_for blacklist: allow_none, heuristics: [match_all]
|
77
|
-
|
79
|
+
binding = nil
|
80
|
+
expect { config.heuristic_for "not an error", binding }
|
78
81
|
.to raise_error ArgumentError, /"not an error"/
|
79
82
|
end
|
80
83
|
|
81
84
|
it 'finds the first heuristic that is willing to accept it' do
|
85
|
+
binding = nil
|
82
86
|
config = config_for blacklist: allow_all,
|
83
87
|
heuristics: [match_no_method_error, match_all]
|
84
88
|
exception = capture { sdfsdfsdf() }
|
85
|
-
expect(config.heuristic_for exception).to be_a_kind_of match_no_method_error
|
86
|
-
expect(config.heuristic_for exception).to_not be_a_kind_of match_all
|
89
|
+
expect(config.heuristic_for exception, binding).to be_a_kind_of match_no_method_error
|
90
|
+
expect(config.heuristic_for exception, binding).to_not be_a_kind_of match_all
|
87
91
|
end
|
88
92
|
end
|
89
93
|
|
@@ -92,11 +96,12 @@ RSpec.describe 'configuration', config: true do
|
|
92
96
|
|
93
97
|
describe 'blacklist' do
|
94
98
|
it 'doesn\'t accept a SystemExit' do
|
99
|
+
binding = nil
|
95
100
|
system_exit = capture { exit 1 }
|
96
|
-
expect(default_config.accept? system_exit).to eq false
|
101
|
+
expect(default_config.accept? system_exit, binding).to eq false
|
97
102
|
|
98
103
|
generic_exception = capture { raise }
|
99
|
-
expect(default_config.accept? generic_exception).to eq true
|
104
|
+
expect(default_config.accept? generic_exception, binding).to eq true
|
100
105
|
end
|
101
106
|
end
|
102
107
|
|
@@ -22,4 +22,54 @@ RSpec.describe 'Heuristic for a NoMethodError', heuristic: true do
|
|
22
22
|
extracts_method_name! '<', "undefined method `<' for nil:NilClass"
|
23
23
|
extracts_method_name! "ab `c' d", "undefined method `ab `c' d' for main:Object"
|
24
24
|
end
|
25
|
+
|
26
|
+
describe 'on nil' do
|
27
|
+
describe 'for an instance variable' do
|
28
|
+
it 'suggest a closely spelled variable name if one exists' do
|
29
|
+
@abcd = 123
|
30
|
+
err = nil
|
31
|
+
begin
|
32
|
+
@abce.even?
|
33
|
+
rescue NoMethodError => no_method_error
|
34
|
+
err = no_method_error
|
35
|
+
end
|
36
|
+
|
37
|
+
heuristic = heuristic_class.new project: build_default_project,
|
38
|
+
einfo: einfo_for(err, binding)
|
39
|
+
expect(heuristic.semantic_explanation).to match /@abcd/
|
40
|
+
expect(heuristic.semantic_explanation).to match /@abce/
|
41
|
+
expect(heuristic.semantic_explanation).to match /spell/
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'does not suggest a misspelling when there is no spelled variable' do
|
45
|
+
@abcd = 123
|
46
|
+
err = nil
|
47
|
+
begin
|
48
|
+
@ablol.even?
|
49
|
+
rescue NoMethodError => no_method_error
|
50
|
+
err = no_method_error
|
51
|
+
end
|
52
|
+
|
53
|
+
heuristic = heuristic_class.new project: build_default_project,
|
54
|
+
einfo: einfo_for(err, binding)
|
55
|
+
expect(heuristic.semantic_explanation).to_not match /spell/
|
56
|
+
expect(heuristic.semantic_explanation).to_not match /@abcd/
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'doesn\'t suggest this when there is no binding provided' do
|
60
|
+
@abcd = 123
|
61
|
+
err = nil
|
62
|
+
begin
|
63
|
+
@abce.even?
|
64
|
+
rescue NoMethodError => no_method_error
|
65
|
+
err = no_method_error
|
66
|
+
end
|
67
|
+
|
68
|
+
heuristic = heuristic_class.new project: build_default_project,
|
69
|
+
einfo: einfo_for(err, nil)
|
70
|
+
expect(heuristic.semantic_explanation).to_not match /spell/
|
71
|
+
expect(heuristic.semantic_explanation).to_not match /@abcd/
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
25
75
|
end
|
@@ -10,7 +10,8 @@ module HeuristicSpecHelpers
|
|
10
10
|
|
11
11
|
def heuristic_for(attributes={})
|
12
12
|
heuristic_class.new project: build_default_project(attributes),
|
13
|
-
einfo: einfo_for(FakeException.new
|
13
|
+
einfo: einfo_for(FakeException.new(attributes),
|
14
|
+
attributes[:binding])
|
14
15
|
end
|
15
16
|
|
16
17
|
def build_default_project(attributes={})
|
@@ -78,7 +78,7 @@ RSpec.describe 'heuristics for the WrongNumberOfArguments', heuristic: true do
|
|
78
78
|
context: (-5..5),
|
79
79
|
emphasis: :code,
|
80
80
|
location: ErrorToCommunicate::ExceptionInfo::Location.new(
|
81
|
-
path: '/a', linenum: 1, label: 'b'
|
81
|
+
path: '/a', linenum: 1, label: 'b', binding: nil
|
82
82
|
)
|
83
83
|
end
|
84
84
|
|
data/spec/heuristic_spec.rb
CHANGED
@@ -4,7 +4,7 @@ require 'heuristic/spec_helper'
|
|
4
4
|
|
5
5
|
RSpec.describe 'Heuristic', heuristic: true do
|
6
6
|
let(:einfo) { ErrorToCommunicate::ExceptionInfo.new classname: 'the classname', message: 'the message', backtrace: [
|
7
|
-
ErrorToCommunicate::ExceptionInfo::Location.new(path: 'file', linenum: 12, label: 'a')
|
7
|
+
ErrorToCommunicate::ExceptionInfo::Location.new(path: 'file', linenum: 12, label: 'a', binding: nil)
|
8
8
|
]
|
9
9
|
}
|
10
10
|
let(:subclass) { Class.new ErrorToCommunicate::Heuristic }
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'error_to_communicate/levenshtein'
|
2
|
+
|
3
|
+
RSpec.describe ErrorToCommunicate::Levenshtein do
|
4
|
+
def assert_distance(distance, target, actual)
|
5
|
+
expect(described_class.call target, actual).to eq distance
|
6
|
+
end
|
7
|
+
|
8
|
+
specify 'count one distance for each substitution' do
|
9
|
+
assert_distance 1, "kitten", "sitten" # "s" for "k"
|
10
|
+
assert_distance 1, "sitten", "sittin" # "i" for "e"
|
11
|
+
assert_distance 2, "sitten", "sitxin" # "i" for "e", and "x" for "t"
|
12
|
+
end
|
13
|
+
|
14
|
+
example 'count one distance for each addition' do
|
15
|
+
assert_distance 1, "sittin", "sitting" # "g"
|
16
|
+
assert_distance 2, "sitin", "sitting" # "t", and "g"
|
17
|
+
assert_distance 2, "cat", "catch" # "ch"
|
18
|
+
end
|
19
|
+
|
20
|
+
example 'count one distance for each deletion' do
|
21
|
+
assert_distance 1, "sitting", "sittin" # "g"
|
22
|
+
assert_distance 2, "sitting", "sitin" # "t", and "g"
|
23
|
+
assert_distance 2, "catch", "cat" # "ch"
|
24
|
+
end
|
25
|
+
|
26
|
+
example 'add the various numbers' do
|
27
|
+
# add "a" at beginning
|
28
|
+
# delete "h" from end
|
29
|
+
# swap "X" to "d"
|
30
|
+
assert_distance 3, "abcdefg", "bcXefgh"
|
31
|
+
end
|
32
|
+
|
33
|
+
describe 'some edge cases' do
|
34
|
+
[ ['--a', 'a', 2],
|
35
|
+
['a', '--a', 2],
|
36
|
+
['-a-', 'a', 2],
|
37
|
+
['a', '-a-', 2],
|
38
|
+
['---', 'a', 3],
|
39
|
+
['a', '---', 3],
|
40
|
+
['aaa', 'bbb', 3],
|
41
|
+
['bbb', 'aaa', 3],
|
42
|
+
].each do |s1, s2, distance|
|
43
|
+
example "distance(#{s1.inspect}, #{s2.inspect}) # => #{distance.inspect}" do
|
44
|
+
assert_distance distance, s1, s2
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
specify 'it is not crazy stupid slow' do
|
50
|
+
# When comparing "aaa", "bbb",
|
51
|
+
# This algorithm compares 94 pairs of strings.
|
52
|
+
# When those pairs are uniqued, there are only 16 left,
|
53
|
+
# so it is doing 78 redundant steps.
|
54
|
+
#
|
55
|
+
# At length 4 ("aaaa", "bbbb"), it does 481 comparisons instead of 25
|
56
|
+
# At length 5, 2524 instead of 36
|
57
|
+
# At length 10, 12_146_179 instead of 121, and it took 52.585554 seconds
|
58
|
+
n = 100
|
59
|
+
start = Time.now
|
60
|
+
assert_distance n, "a"*n, "b"*n
|
61
|
+
time = Time.now - start
|
62
|
+
expect(time).to be < 1
|
63
|
+
end
|
64
|
+
end
|
@@ -63,7 +63,8 @@ RSpec.describe 'Parsing exceptions to ExceptionInfo', einfo: true do
|
|
63
63
|
# it 'records the relative filepath if it cannot fild the file'
|
64
64
|
|
65
65
|
def assert_parses_line(line, assertions)
|
66
|
-
|
66
|
+
binding = nil
|
67
|
+
parsed = ErrorToCommunicate::ExceptionInfo::Location.parse(line, binding)
|
67
68
|
assertions.each do |method_name, expected|
|
68
69
|
expected = Pathname.new expected if method_name == :path
|
69
70
|
actual = parsed.__send__ method_name
|
@@ -85,11 +86,14 @@ RSpec.describe 'Parsing exceptions to ExceptionInfo', einfo: true do
|
|
85
86
|
|
86
87
|
context 'fake backtraces (eg RSpec renders text in the `formatted_backtrace` to get it to print messages there)' do
|
87
88
|
it 'has an empty path, linenum of -1, the entire string is the label' do
|
88
|
-
|
89
|
-
|
90
|
-
"",
|
91
|
-
|
92
|
-
|
89
|
+
binding = nil
|
90
|
+
a, b, c = parsed = ErrorToCommunicate::ExceptionInfo.parse_backtrace(
|
91
|
+
[ "/Users/josh/.gem/ruby/2.1.1/gems/rspec-core-3.2.3/lib/rspec/core/runner.rb:29:in `block in autorun'",
|
92
|
+
"",
|
93
|
+
" Showing full backtrace because every line was filtered out.",
|
94
|
+
],
|
95
|
+
binding
|
96
|
+
)
|
93
97
|
|
94
98
|
expect(parsed.map(&:path).map(&:to_s)).to eq [
|
95
99
|
"/Users/josh/.gem/ruby/2.1.1/gems/rspec-core-3.2.3/lib/rspec/core/runner.rb",
|
@@ -158,6 +162,7 @@ RSpec.describe 'Parsing exceptions to ExceptionInfo', einfo: true do
|
|
158
162
|
|
159
163
|
describe 'ExceptionInfo::Location' do
|
160
164
|
def location_for(attrs)
|
165
|
+
attrs[:binding] ||= nil
|
161
166
|
ErrorToCommunicate::ExceptionInfo::Location.new attrs
|
162
167
|
end
|
163
168
|
|
@@ -122,7 +122,8 @@ RSpec.describe ErrorToCommunicate::RSpecFormatter, rspec_formatter: true do
|
|
122
122
|
heuristic = ErrorToCommunicate::Heuristic::RSpecFailure.new \
|
123
123
|
config: ErrorToCommunicate::Config.default,
|
124
124
|
failure: failure_for(description: 'DESC', type: :assertion),
|
125
|
-
failure_number: 999
|
125
|
+
failure_number: 999,
|
126
|
+
binding: binding
|
126
127
|
summaryname, ((columnsname, *columns)) = heuristic.semantic_summary
|
127
128
|
expect(summaryname).to eq :summary
|
128
129
|
expect(columnsname).to eq :columns
|
@@ -132,9 +133,10 @@ RSpec.describe ErrorToCommunicate::RSpecFormatter, rspec_formatter: true do
|
|
132
133
|
specify 'the info is the formatted error message and first line from the backtrace with some context' do
|
133
134
|
config = ErrorToCommunicate::Config.new
|
134
135
|
heuristic = ErrorToCommunicate::Heuristic::RSpecFailure.new \
|
135
|
-
config:
|
136
|
-
failure:
|
137
|
-
failure_number: 999
|
136
|
+
config: ErrorToCommunicate::Config.default,
|
137
|
+
failure: failure_for(type: :assertion, message: 'MESSAGE', formatted_backtrace: ["/file:123:in `method'"]),
|
138
|
+
failure_number: 999,
|
139
|
+
binding: binding
|
138
140
|
heuristicname, ((messagename, message), (codename, codeattrs), *rest) = heuristic.semantic_info
|
139
141
|
expect(heuristicname).to eq :heuristic
|
140
142
|
expect(messagename ).to eq :message
|
@@ -151,7 +153,8 @@ RSpec.describe ErrorToCommunicate::RSpecFormatter, rspec_formatter: true do
|
|
151
153
|
heuristic = ErrorToCommunicate::Heuristic::RSpecFailure.new \
|
152
154
|
config: ErrorToCommunicate::Config.default,
|
153
155
|
failure_number: 999,
|
154
|
-
failure: failure_for(type: :assertion, message: 'MESSAGE', formatted_backtrace: [])
|
156
|
+
failure: failure_for(type: :assertion, message: 'MESSAGE', formatted_backtrace: []),
|
157
|
+
binding: binding
|
155
158
|
heuristicname, ((messagename, message), *rest) = heuristic.semantic_info
|
156
159
|
expect(heuristicname).to eq :heuristic
|
157
160
|
expect(messagename ).to eq :message
|
@@ -180,7 +183,8 @@ RSpec.describe ErrorToCommunicate::RSpecFormatter, rspec_formatter: true do
|
|
180
183
|
failure: failure_for(message: "wrong number of arguments (1 for 0)",
|
181
184
|
description: 'DESC',
|
182
185
|
type: :argument_error),
|
183
|
-
failure_number: 999
|
186
|
+
failure_number: 999,
|
187
|
+
binding: binding
|
184
188
|
summaryname, ((columnsname, *columns)) = heuristic.semantic_summary
|
185
189
|
expect(summaryname).to eq :summary
|
186
190
|
expect(columnsname).to eq :columns
|
@@ -193,12 +197,34 @@ RSpec.describe ErrorToCommunicate::RSpecFormatter, rspec_formatter: true do
|
|
193
197
|
.to receive(:semantic_info).and_return("SEMANTICINFO")
|
194
198
|
|
195
199
|
heuristic = ErrorToCommunicate::Heuristic::RSpecFailure.new \
|
196
|
-
config:
|
197
|
-
failure:
|
198
|
-
failure_number: 999
|
200
|
+
config: ErrorToCommunicate::Config.new,
|
201
|
+
failure: failure_for(message: "wrong number of arguments (1 for 0)", type: :argument_error),
|
202
|
+
failure_number: 999,
|
203
|
+
binding: binding
|
199
204
|
|
200
205
|
expect(heuristic.semantic_info).to eq "SEMANTICINFO"
|
201
206
|
end
|
207
|
+
|
208
|
+
it 'provides the bindings needed for some of the advanced analysis' do
|
209
|
+
formatter = new_formatter
|
210
|
+
context_around_failure = this_line_of_code
|
211
|
+
run_specs_against formatter do
|
212
|
+
example('suggests better name1') {
|
213
|
+
@abc = 123
|
214
|
+
@abd.even? # misspelled
|
215
|
+
}
|
216
|
+
example('suggests better name2') {
|
217
|
+
@lol = 123
|
218
|
+
@lul.even? # misspelled
|
219
|
+
}
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
# Sigh the above whitespace is important, or it can pass because the assertion is in the code displayed for context
|
224
|
+
# It's stupid, I know
|
225
|
+
expect(get_printed formatter).to include "Possible misspelling of `@lol'"
|
226
|
+
expect(get_printed formatter).to include "Possible misspelling of `@abc'"
|
227
|
+
end
|
202
228
|
end
|
203
229
|
|
204
230
|
context 'fixing the message\'s whitespace' do
|
data/spec/spec_helper.rb
CHANGED
@@ -12,8 +12,8 @@ class FakeException
|
|
12
12
|
end
|
13
13
|
|
14
14
|
module SpecHelpers
|
15
|
-
def einfo_for(exception)
|
16
|
-
ErrorToCommunicate::ExceptionInfo.parse exception
|
15
|
+
def einfo_for(exception, binding=nil)
|
16
|
+
ErrorToCommunicate::ExceptionInfo.parse exception, binding
|
17
17
|
end
|
18
18
|
|
19
19
|
def trap_warnings
|
@@ -11,8 +11,8 @@ Gem::Specification.new do |s|
|
|
11
11
|
s.homepage = 'https://github.com/JoshCheek/what-we-ve-got-here-is-an-error-to-communicate'
|
12
12
|
|
13
13
|
s.add_runtime_dependency 'rouge', '~> 1.8', '!= 1.9.1'
|
14
|
-
|
15
|
-
|
14
|
+
s.add_runtime_dependency 'interception', '~> 0.5'
|
15
|
+
s.add_runtime_dependency 'binding_of_caller', '~> 0.7.2'
|
16
16
|
|
17
17
|
s.add_development_dependency 'rspec', '~> 3.2'
|
18
18
|
s.add_development_dependency 'haiti', '< 0.3', '>= 0.2.0'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: what_weve_got_here_is_an_error_to_communicate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josh Cheek
|
@@ -9,94 +9,122 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-
|
12
|
+
date: 2015-12-24 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rouge
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
requirements:
|
18
|
-
- - ~>
|
18
|
+
- - "~>"
|
19
19
|
- !ruby/object:Gem::Version
|
20
20
|
version: '1.8'
|
21
|
-
- -
|
21
|
+
- - "!="
|
22
22
|
- !ruby/object:Gem::Version
|
23
23
|
version: 1.9.1
|
24
24
|
type: :runtime
|
25
25
|
prerelease: false
|
26
26
|
version_requirements: !ruby/object:Gem::Requirement
|
27
27
|
requirements:
|
28
|
-
- - ~>
|
28
|
+
- - "~>"
|
29
29
|
- !ruby/object:Gem::Version
|
30
30
|
version: '1.8'
|
31
|
-
- -
|
31
|
+
- - "!="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: 1.9.1
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: interception
|
36
|
+
requirement: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.5'
|
41
|
+
type: :runtime
|
42
|
+
prerelease: false
|
43
|
+
version_requirements: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.5'
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: binding_of_caller
|
50
|
+
requirement: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.7.2
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.7.2
|
34
62
|
- !ruby/object:Gem::Dependency
|
35
63
|
name: rspec
|
36
64
|
requirement: !ruby/object:Gem::Requirement
|
37
65
|
requirements:
|
38
|
-
- - ~>
|
66
|
+
- - "~>"
|
39
67
|
- !ruby/object:Gem::Version
|
40
68
|
version: '3.2'
|
41
69
|
type: :development
|
42
70
|
prerelease: false
|
43
71
|
version_requirements: !ruby/object:Gem::Requirement
|
44
72
|
requirements:
|
45
|
-
- - ~>
|
73
|
+
- - "~>"
|
46
74
|
- !ruby/object:Gem::Version
|
47
75
|
version: '3.2'
|
48
76
|
- !ruby/object:Gem::Dependency
|
49
77
|
name: haiti
|
50
78
|
requirement: !ruby/object:Gem::Requirement
|
51
79
|
requirements:
|
52
|
-
- - <
|
80
|
+
- - "<"
|
53
81
|
- !ruby/object:Gem::Version
|
54
82
|
version: '0.3'
|
55
|
-
- -
|
83
|
+
- - ">="
|
56
84
|
- !ruby/object:Gem::Version
|
57
85
|
version: 0.2.0
|
58
86
|
type: :development
|
59
87
|
prerelease: false
|
60
88
|
version_requirements: !ruby/object:Gem::Requirement
|
61
89
|
requirements:
|
62
|
-
- - <
|
90
|
+
- - "<"
|
63
91
|
- !ruby/object:Gem::Version
|
64
92
|
version: '0.3'
|
65
|
-
- -
|
93
|
+
- - ">="
|
66
94
|
- !ruby/object:Gem::Version
|
67
95
|
version: 0.2.0
|
68
96
|
- !ruby/object:Gem::Dependency
|
69
97
|
name: pry
|
70
98
|
requirement: !ruby/object:Gem::Requirement
|
71
99
|
requirements:
|
72
|
-
- - <
|
100
|
+
- - "<"
|
73
101
|
- !ruby/object:Gem::Version
|
74
102
|
version: '0.11'
|
75
|
-
- -
|
103
|
+
- - ">="
|
76
104
|
- !ruby/object:Gem::Version
|
77
105
|
version: 0.10.0
|
78
106
|
type: :development
|
79
107
|
prerelease: false
|
80
108
|
version_requirements: !ruby/object:Gem::Requirement
|
81
109
|
requirements:
|
82
|
-
- - <
|
110
|
+
- - "<"
|
83
111
|
- !ruby/object:Gem::Version
|
84
112
|
version: '0.11'
|
85
|
-
- -
|
113
|
+
- - ">="
|
86
114
|
- !ruby/object:Gem::Version
|
87
115
|
version: 0.10.0
|
88
116
|
- !ruby/object:Gem::Dependency
|
89
117
|
name: rake
|
90
118
|
requirement: !ruby/object:Gem::Requirement
|
91
119
|
requirements:
|
92
|
-
- - ~>
|
120
|
+
- - "~>"
|
93
121
|
- !ruby/object:Gem::Version
|
94
122
|
version: '10.4'
|
95
123
|
type: :development
|
96
124
|
prerelease: false
|
97
125
|
version_requirements: !ruby/object:Gem::Requirement
|
98
126
|
requirements:
|
99
|
-
- - ~>
|
127
|
+
- - "~>"
|
100
128
|
- !ruby/object:Gem::Version
|
101
129
|
version: '10.4'
|
102
130
|
description: Hooks into program lifecycle to display error messages to you in a helpufl
|
@@ -106,9 +134,9 @@ executables: []
|
|
106
134
|
extensions: []
|
107
135
|
extra_rdoc_files: []
|
108
136
|
files:
|
109
|
-
- .gitignore
|
110
|
-
- .rspec
|
111
|
-
- .travis.yml
|
137
|
+
- ".gitignore"
|
138
|
+
- ".rspec"
|
139
|
+
- ".travis.yml"
|
112
140
|
- Gemfile
|
113
141
|
- Rakefile
|
114
142
|
- Readme.md
|
@@ -129,6 +157,7 @@ files:
|
|
129
157
|
- lib/error_to_communicate/heuristic/no_method_error.rb
|
130
158
|
- lib/error_to_communicate/heuristic/syntax_error.rb
|
131
159
|
- lib/error_to_communicate/heuristic/wrong_number_of_arguments.rb
|
160
|
+
- lib/error_to_communicate/levenshtein.rb
|
132
161
|
- lib/error_to_communicate/project.rb
|
133
162
|
- lib/error_to_communicate/rspec_formatter.rb
|
134
163
|
- lib/error_to_communicate/theme.rb
|
@@ -144,6 +173,7 @@ files:
|
|
144
173
|
- spec/acceptance/short_and_long_require_spec.rb
|
145
174
|
- spec/acceptance/spec_helper.rb
|
146
175
|
- spec/acceptance/syntax_error_spec.rb
|
176
|
+
- spec/acceptance/unexpected_nil_spec.rb
|
147
177
|
- spec/acceptance/wrong_number_of_arguments_spec.rb
|
148
178
|
- spec/config_spec.rb
|
149
179
|
- spec/heuristic/exception_spec.rb
|
@@ -152,6 +182,7 @@ files:
|
|
152
182
|
- spec/heuristic/spec_helper.rb
|
153
183
|
- spec/heuristic/wrong_number_of_arguments_spec.rb
|
154
184
|
- spec/heuristic_spec.rb
|
185
|
+
- spec/levenshtein_spec.rb
|
155
186
|
- spec/parsing_exception_info_spec.rb
|
156
187
|
- spec/rspec_formatter_spec.rb
|
157
188
|
- spec/spec_helper.rb
|
@@ -166,17 +197,17 @@ require_paths:
|
|
166
197
|
- lib
|
167
198
|
required_ruby_version: !ruby/object:Gem::Requirement
|
168
199
|
requirements:
|
169
|
-
- -
|
200
|
+
- - ">="
|
170
201
|
- !ruby/object:Gem::Version
|
171
202
|
version: '0'
|
172
203
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
173
204
|
requirements:
|
174
|
-
- -
|
205
|
+
- - ">="
|
175
206
|
- !ruby/object:Gem::Version
|
176
207
|
version: '0'
|
177
208
|
requirements: []
|
178
209
|
rubyforge_project:
|
179
|
-
rubygems_version: 2.4.
|
210
|
+
rubygems_version: 2.4.8
|
180
211
|
signing_key:
|
181
212
|
specification_version: 4
|
182
213
|
summary: Readable, helpful error messages
|