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