what_weve_got_here_is_an_error_to_communicate 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/Readme.md +9 -7
- data/experiments/formatting/other_resources +7 -0
- data/lib/error_to_communicate/at_exit.rb +15 -9
- data/lib/error_to_communicate/config.rb +55 -22
- data/lib/error_to_communicate/exception_info.rb +86 -31
- data/lib/error_to_communicate/format_terminal.rb +146 -0
- data/lib/error_to_communicate/heuristic/exception.rb +22 -0
- data/lib/error_to_communicate/heuristic/load_error.rb +57 -0
- data/lib/error_to_communicate/heuristic/no_method_error.rb +35 -0
- data/lib/error_to_communicate/heuristic/syntax_error.rb +55 -0
- data/lib/error_to_communicate/heuristic/wrong_number_of_arguments.rb +73 -0
- data/lib/error_to_communicate/heuristic.rb +54 -0
- data/lib/error_to_communicate/project.rb +50 -0
- data/lib/error_to_communicate/rspec_formatter.rb +8 -9
- data/lib/error_to_communicate/theme.rb +137 -0
- data/lib/error_to_communicate/version.rb +2 -2
- data/lib/error_to_communicate.rb +4 -2
- data/spec/acceptance/exception_spec.rb +2 -4
- data/spec/acceptance/load_error_spec.rb +23 -0
- data/spec/acceptance/name_error_spec.rb +46 -0
- data/spec/acceptance/no_methood_error_spec.rb +6 -8
- data/spec/acceptance/runtime_error_spec.rb +27 -0
- data/spec/acceptance/short_and_long_require_spec.rb +29 -0
- data/spec/acceptance/spec_helper.rb +4 -3
- data/spec/acceptance/syntax_error_spec.rb +32 -0
- data/spec/acceptance/{argument_error_spec.rb → wrong_number_of_arguments_spec.rb} +1 -1
- data/spec/config_spec.rb +120 -0
- data/spec/heuristic/exception_spec.rb +17 -0
- data/spec/heuristic/load_error_spec.rb +195 -0
- data/spec/heuristic/no_method_error_spec.rb +25 -0
- data/spec/heuristic/spec_helper.rb +33 -0
- data/spec/heuristic/wrong_number_of_arguments_spec.rb +115 -0
- data/spec/heuristic_spec.rb +76 -0
- data/spec/parsing_exception_info_spec.rb +212 -0
- data/spec/rspec_formatter_spec.rb +3 -1
- data/spec/spec_helper.rb +28 -1
- data/what_weve_got_here_is_an_error_to_communicate.gemspec +2 -2
- metadata +29 -19
- data/lib/error_to_communicate/format/terminal_helpers.rb +0 -97
- data/lib/error_to_communicate/format.rb +0 -132
- data/lib/error_to_communicate/parse/backtrace.rb +0 -34
- data/lib/error_to_communicate/parse/exception.rb +0 -21
- data/lib/error_to_communicate/parse/no_method_error.rb +0 -27
- data/lib/error_to_communicate/parse/registry.rb +0 -30
- data/lib/error_to_communicate/parse/wrong_number_of_arguments.rb +0 -35
- data/spec/parse/backtrace_spec.rb +0 -101
- data/spec/parse/exception_spec.rb +0 -14
- data/spec/parse/no_method_error_spec.rb +0 -23
- data/spec/parse/registered_parsers_spec.rb +0 -68
- data/spec/parse/spec_helper.rb +0 -23
- data/spec/parse/wrong_number_of_arguments_spec.rb +0 -77
data/spec/config_spec.rb
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'error_to_communicate/config'
|
2
|
+
|
3
|
+
RSpec.describe 'configuration', config: true do
|
4
|
+
# Subclassing to make it a easier to refer to, and to get a new instance
|
5
|
+
# (config_class.default will not be affected by changes to Config.default)
|
6
|
+
let(:config_class) { Class.new ErrorToCommunicate::Config }
|
7
|
+
|
8
|
+
# named blacklists
|
9
|
+
let(:allow_all) { lambda { |e| false } }
|
10
|
+
let(:allow_none) { lambda { |e| true } }
|
11
|
+
|
12
|
+
# named heuristics
|
13
|
+
let :match_all do
|
14
|
+
ErrorToCommunicate::Heuristic::Exception
|
15
|
+
end
|
16
|
+
|
17
|
+
let :match_no_method_error do
|
18
|
+
ErrorToCommunicate::Heuristic::NoMethodError
|
19
|
+
end
|
20
|
+
|
21
|
+
# helper methods
|
22
|
+
def yes_accept!(config, ex)
|
23
|
+
expect(config.accept? ex).to eq true
|
24
|
+
end
|
25
|
+
|
26
|
+
def no_accept!(config, ex)
|
27
|
+
expect(config.accept? ex).to eq false
|
28
|
+
end
|
29
|
+
|
30
|
+
def config_for(attrs)
|
31
|
+
config_class.new attrs
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '.default' do
|
35
|
+
it 'is a memoized' do
|
36
|
+
expect(config_class.default).to equal config_class.default
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'is an instance of Config' do
|
40
|
+
expect(config_class.default).to be_a_kind_of config_class
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'uses the default heuristics and blacklist (behaviour described below)' do
|
44
|
+
expect(config_class.default.heuristics).to equal config_class::DEFAULT_HEURISTICS
|
45
|
+
expect(config_class.default.blacklist ).to equal config_class::DEFAULT_BLACKLIST
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'accepting an exception' do
|
50
|
+
it 'doesn\'t accept non-exception-looking things -- if it can\'t parse it, then we should let the default process take place (eg exception on another system)' do
|
51
|
+
config = config_for blacklist: allow_all, heuristics: [match_all]
|
52
|
+
|
53
|
+
no_accept! config, nil
|
54
|
+
no_accept! config, "omg"
|
55
|
+
no_accept! config, Struct.new(:message).new('')
|
56
|
+
no_accept! config, Struct.new(:backtrace).new([])
|
57
|
+
|
58
|
+
yes_accept! config, Struct.new(:message, :backtrace).new('', [])
|
59
|
+
yes_accept! config, capture { raise }
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'does not accept anything from its blacklist' do
|
63
|
+
config = config_for blacklist: allow_none, heuristics: [match_all]
|
64
|
+
no_accept! config, capture { raise }
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'accepts anything not blacklisted, that it has a heuristic for' do
|
68
|
+
config = config_for blacklist: allow_all, heuristics: [match_no_method_error]
|
69
|
+
yes_accept! config, capture { jjj() }
|
70
|
+
no_accept! config, capture { raise }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe 'finding the heuristic for an exception' do
|
75
|
+
it 'raises an ArgumentError if given an acception that it won\'t accept' do
|
76
|
+
config = config_for blacklist: allow_none, heuristics: [match_all]
|
77
|
+
expect { config.heuristic_for "not an error" }
|
78
|
+
.to raise_error ArgumentError, /"not an error"/
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'finds the first heuristic that is willing to accept it' do
|
82
|
+
config = config_for blacklist: allow_all,
|
83
|
+
heuristics: [match_no_method_error, match_all]
|
84
|
+
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
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe 'The default configuration' do
|
91
|
+
let(:default_config) { config_class.new }
|
92
|
+
|
93
|
+
describe 'blacklist' do
|
94
|
+
it 'doesn\'t accept a SystemExit' do
|
95
|
+
system_exit = capture { exit 1 }
|
96
|
+
expect(default_config.accept? system_exit).to eq false
|
97
|
+
|
98
|
+
generic_exception = capture { raise }
|
99
|
+
expect(default_config.accept? generic_exception).to eq true
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe 'heuristics (correct selection is tested in spec/acceptance)' do
|
104
|
+
it 'has heuristics for WrongNumberOfArguments' do
|
105
|
+
expect(default_config.heuristics).to include \
|
106
|
+
ErrorToCommunicate::Heuristic::WrongNumberOfArguments
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'has heuristics for NoMethodError' do
|
110
|
+
expect(default_config.heuristics).to include \
|
111
|
+
ErrorToCommunicate::Heuristic::NoMethodError
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'has heuristics for Exception' do
|
115
|
+
expect(default_config.heuristics).to include \
|
116
|
+
ErrorToCommunicate::Heuristic::Exception
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'heuristic/spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe 'Heuristic for a Exception', heuristic: true do
|
4
|
+
def heuristic_class
|
5
|
+
ErrorToCommunicate::Heuristic::Exception
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'is for every type of exception (via inheritance)' do
|
9
|
+
is_for! RuntimeError.new
|
10
|
+
is_for! Exception.new('omg')
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'uses the exception message as is explanation' do
|
14
|
+
einfo = heuristic_for(message: 'message from exception')
|
15
|
+
expect(einfo.explanation).to eq 'message from exception'
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'heuristic/spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe 'Heuristic for a LoadError', heuristic: true do
|
4
|
+
def heuristic_class
|
5
|
+
ErrorToCommunicate::Heuristic::LoadError
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'is for LoadErrors with a message that includes " -- " to separate the message from the path' do
|
9
|
+
is_for! LoadError.new 'cannot load such file -- a/b/c'
|
10
|
+
is_for! LoadError.new "no such file to load -- /a/b/c\ndef\tghi\ejkl"
|
11
|
+
is_for! LoadError.new(' -- ')
|
12
|
+
is_not_for! LoadError.new('-- ')
|
13
|
+
is_not_for! LoadError.new(' --')
|
14
|
+
is_not_for! LoadError.new('whatever')
|
15
|
+
is_not_for! LoadError.new('what-ever')
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'methods that can lead to this error' do
|
19
|
+
it('works for require') { is_for! capture { require 'a/b/c' } }
|
20
|
+
it('works for require_relative') { is_for! capture { require_relative 'a/b/c' } }
|
21
|
+
it('works for load') { is_for! capture { load 'a/b/c' } }
|
22
|
+
end
|
23
|
+
|
24
|
+
describe 'identifying the missing path' do
|
25
|
+
def heuristic_for_path(path)
|
26
|
+
heuristic_for message: "cannot load such file -- #{path}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def identifies!(path, expected=path)
|
30
|
+
heuristic_for_path(path).tap { |h| expect(h.path).to eq Pathname.new(expected) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def absolute!(path)
|
34
|
+
heuristic = heuristic_for_path(path)
|
35
|
+
expect(heuristic).to be_absolute
|
36
|
+
expect(heuristic).to_not be_relative
|
37
|
+
end
|
38
|
+
|
39
|
+
def relative!(path)
|
40
|
+
heuristic = heuristic_for_path(path)
|
41
|
+
expect(heuristic).to_not be_absolute
|
42
|
+
expect(heuristic).to be_relative
|
43
|
+
end
|
44
|
+
|
45
|
+
it('identifies paths without directories') { identifies! 'thepath' }
|
46
|
+
it('identifies paths with underscores') { identifies! 'the_path' }
|
47
|
+
it('identifies paths within directories') { identifies! 'this/is/a/path' }
|
48
|
+
it('identifies empty strings') { identifies! "" }
|
49
|
+
it('identifies empty space filenames') { identifies! " " }
|
50
|
+
it('identifies filenames that begin with spaces') { identifies! " abc" }
|
51
|
+
it('identifies filenames that end with spaces') { identifies! "abc " }
|
52
|
+
it('identifies paths with whitespace') { identifies! "a b" }
|
53
|
+
it('identifies paths with dashes') { identifies! 'a-b - c - d' }
|
54
|
+
it('identifies paths with double dashes') { identifies! 'a--b -- c -- d' }
|
55
|
+
it('identifies paths with escaped characters') { identifies! 'a\nb\tc\ed' }
|
56
|
+
# Not sure if it should do this, or provide both the relative and absolute path
|
57
|
+
# it 'expands paths from the home-dir to be absolute' do
|
58
|
+
# identifies! '/a/b/c', '/a/b/c'
|
59
|
+
# identifies! 'a/b/c', 'a/b/c'
|
60
|
+
# identifies! '~/a/b/c', "#{ENV['HOME']}/a/b/c"
|
61
|
+
# identifies! '~a/b/c', '~a/b/c'
|
62
|
+
# end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe 'identification of the relevant locations from the backtrace' do
|
66
|
+
it 'identifies the first line that is outside of rubygems' do
|
67
|
+
heuristic = heuristic_for backtrace: [
|
68
|
+
"/a/b:1:in `a'", "/a/c:2:in `c'", "/d:3:in `d'", "/e:4:in `e'"
|
69
|
+
], rubygems_dir: '/a'
|
70
|
+
expect(heuristic.first_nongem_line.path.to_s).to eq '/d'
|
71
|
+
|
72
|
+
heuristic = heuristic_for backtrace: [
|
73
|
+
"/b:2:in `b'", "/c:3:in `c'"
|
74
|
+
], rubygems_dir: '/a'
|
75
|
+
expect(heuristic.first_nongem_line.path.to_s).to eq '/b'
|
76
|
+
|
77
|
+
heuristic = heuristic_for backtrace: [
|
78
|
+
"/a/b:1:in `a'", "/c:2:in `c'", "/d:3:in `d'"
|
79
|
+
], rubygems_dir: '/a/b'
|
80
|
+
expect(heuristic.first_nongem_line.path.to_s).to eq '/c'
|
81
|
+
|
82
|
+
heuristic = heuristic_for backtrace: [
|
83
|
+
"/a/b:1:in `a'", "/c:2:in `c'", "/d:3:in `d'"
|
84
|
+
], rubygems_dir: '/'
|
85
|
+
expect(heuristic.first_nongem_line).to eq nil
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'identifies the line within the project root' do
|
89
|
+
heuristic = heuristic_for backtrace: [
|
90
|
+
"/a/b:1:in `a'", "/c:2:in `c'", "/d:3:in `d'"
|
91
|
+
], root: '/a'
|
92
|
+
expect(heuristic.first_line_within_lib.path.to_s).to eq '/a/b'
|
93
|
+
|
94
|
+
heuristic = heuristic_for backtrace: [
|
95
|
+
"/a/b:1:in `a'", "/c/d:2:in `c'", "/e:3:in `e'"
|
96
|
+
], root: '/c'
|
97
|
+
expect(heuristic.first_line_within_lib.path.to_s).to eq '/c/d'
|
98
|
+
|
99
|
+
heuristic = heuristic_for backtrace: [
|
100
|
+
"/a/b:1:in `a'", "/c/d:2:in `c'", "/e:3:in `e'"
|
101
|
+
], root: '/x'
|
102
|
+
expect(heuristic.first_line_within_lib).to eq nil
|
103
|
+
end
|
104
|
+
|
105
|
+
specify 'when they are different, it displays them both' do
|
106
|
+
heuristic = heuristic_for backtrace: [
|
107
|
+
"/a/b:1:in `a'", "/c/d:2:in `c'", "/e:3:in `e'"
|
108
|
+
], root: '/c'
|
109
|
+
expect(heuristic.relevant_locations.map(&:path).map(&:to_s)).to eq ['/a/b', '/c/d']
|
110
|
+
end
|
111
|
+
|
112
|
+
specify 'when they are the same, it only displays them once' do
|
113
|
+
heuristic = heuristic_for backtrace: [
|
114
|
+
"/a/b:1:in `a'", "/c/d:2:in `c'", "/e:3:in `e'"
|
115
|
+
], root: '/a'
|
116
|
+
expect(heuristic.relevant_locations.map(&:path).map(&:to_s)).to eq ['/a/b']
|
117
|
+
end
|
118
|
+
|
119
|
+
specify 'when either is missing, it displays the other' do
|
120
|
+
heuristic = heuristic_for backtrace: ["/a/b:1:in `a'"], root: '/b'
|
121
|
+
expect(heuristic.relevant_locations.map(&:path).map(&:to_s)).to eq ['/a/b']
|
122
|
+
end
|
123
|
+
|
124
|
+
specify 'when both are missing, it displays a message stating this' do
|
125
|
+
heuristic = heuristic_for backtrace: ["/a/b:1:in `a'"],
|
126
|
+
root: '/b',
|
127
|
+
rubygems_dir: '/a'
|
128
|
+
expect(heuristic.relevant_locations).to eq []
|
129
|
+
name, (context, message) = heuristic.semantic_info
|
130
|
+
expect(name).to eq :heuristic
|
131
|
+
expect(context).to eq :context
|
132
|
+
expect(message).to be_a_kind_of String
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
describe 'displaying relevant code' do
|
137
|
+
let :semantic_code do
|
138
|
+
_heuristic, ((_code, code_attrs), *) =
|
139
|
+
heuristic_for(root: '/a',
|
140
|
+
backtrace: ["/a/b:1:in `a'"],
|
141
|
+
message: 'cannot load such file -- zomg'
|
142
|
+
).semantic_info
|
143
|
+
expect(code_attrs[:location].path.to_s).to eq '/a/b'
|
144
|
+
code_attrs
|
145
|
+
end
|
146
|
+
|
147
|
+
# TODO: Should 5 be configurable?
|
148
|
+
it 'includes 5 lines of context before/after' do
|
149
|
+
expect(semantic_code[:context]).to eq (-5..5)
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'emphasizes the code' do
|
153
|
+
expect(semantic_code[:emphasis]).to eq :code
|
154
|
+
end
|
155
|
+
|
156
|
+
context 'when the line includes the path' do
|
157
|
+
it 'has the message "Couldn\'t find file"' do
|
158
|
+
skip 'Waiting b/c it\'s inconvenient right now to get the code in the heuristic'
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
context 'when the line does not include the path' do
|
163
|
+
it 'has the message "Couldn\'t find \"<FILE>\""' do
|
164
|
+
expect(semantic_code[:message]).to eq 'Couldn\'t find "zomg"'
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Maybe eventuallly:
|
170
|
+
#
|
171
|
+
# it identifies that they are trying to require a file from a gem they don't have
|
172
|
+
# When the require statement is relative (require "./something")
|
173
|
+
# it identifies that they could have required it if run from a different directory
|
174
|
+
# it gives them the `require_relative` path
|
175
|
+
# it tells them how to set up the $LOAD_PATH to avoid the need for such things
|
176
|
+
# it tells them where they would need to make the file, and warns of the danger of this approach
|
177
|
+
# When using require_relative
|
178
|
+
# it identifies misspellings in the require statement
|
179
|
+
# it identifies candidate files that could be required if the path were different
|
180
|
+
# it tells them where they would need to make the file
|
181
|
+
# When the require statement is not relative (require "something")
|
182
|
+
# it identifies misspellings for files they could have required
|
183
|
+
# it identifies that they meant to require something from a gem
|
184
|
+
# if not using Bundler, tells them to `bundle exec` it
|
185
|
+
# if using Bundler
|
186
|
+
# if the gem is not part of the Gemfile, tells them how to add it
|
187
|
+
# if the gem is part of the Gemfile, suggests they check versions
|
188
|
+
# it tells them places they could make the file that are within their lib
|
189
|
+
#
|
190
|
+
# It could possibly look at constant names that they use after the require statement,
|
191
|
+
# if any of them look sufficiently similar to the require statement,
|
192
|
+
# assuming that the require statement was intended to make that constant available
|
193
|
+
# if it can then identify where the constant is defined,
|
194
|
+
# then it could tell them which file to require to get it.
|
195
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'heuristic/spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe 'Heuristic for a NoMethodError', heuristic: true do
|
4
|
+
def heuristic_class
|
5
|
+
ErrorToCommunicate::Heuristic::NoMethodError
|
6
|
+
end
|
7
|
+
|
8
|
+
def extracts_method_name!(expected, message)
|
9
|
+
heuristic = heuristic_for message: message
|
10
|
+
expect(heuristic.undefined_method_name).to eq expected
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'is for NoMethodErrors and NameErrors where it can parse out the missing name' do
|
14
|
+
is_for! NoMethodError.new("undefined method `backtrace4' for Test:Module")
|
15
|
+
is_for! NameError.new("undefined local variable or method `backtrace4' for Test:Module")
|
16
|
+
is_not_for! NoMethodError.new("abc")
|
17
|
+
is_not_for! NameError.new("abc")
|
18
|
+
is_not_for! Exception.new("undefined local variable or method `backtrace4' for Test:Module") # do we actually want to assert this?
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'extracts the name of the method that was called' do
|
22
|
+
extracts_method_name! '<', "undefined method `<' for nil:NilClass"
|
23
|
+
extracts_method_name! "ab `c' d", "undefined method `ab `c' d' for main:Object"
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'error_to_communicate/heuristic'
|
3
|
+
|
4
|
+
module HeuristicSpecHelpers
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def heuristic_class
|
8
|
+
raise NotImplementedError, 'You need to define the heuristic class!'
|
9
|
+
end
|
10
|
+
|
11
|
+
def heuristic_for(attributes={})
|
12
|
+
heuristic_class.new project: build_default_project(attributes),
|
13
|
+
einfo: einfo_for(FakeException.new attributes)
|
14
|
+
end
|
15
|
+
|
16
|
+
def build_default_project(attributes={})
|
17
|
+
ErrorToCommunicate::Project.new \
|
18
|
+
rubygems_dir: attributes.delete(:rubygems_dir),
|
19
|
+
root: attributes.delete(:root)
|
20
|
+
end
|
21
|
+
|
22
|
+
def is_for!(exception)
|
23
|
+
expect(heuristic_class).to be_for einfo_for(exception)
|
24
|
+
end
|
25
|
+
|
26
|
+
def is_not_for!(exception)
|
27
|
+
expect(heuristic_class).to_not be_for einfo_for(exception)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
RSpec.configure do |config|
|
32
|
+
config.include HeuristicSpecHelpers, heuristic: true
|
33
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'heuristic/spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe 'heuristics for the WrongNumberOfArguments', t:true, heuristic: true do
|
4
|
+
def heuristic_class
|
5
|
+
ErrorToCommunicate::Heuristic::WrongNumberOfArguments
|
6
|
+
end
|
7
|
+
|
8
|
+
describe '.for?' do
|
9
|
+
it 'is true when given an MRI style wrong number of arguments message' do
|
10
|
+
is_for! ArgumentError.new "wrong number of arguments (1 for 0)"
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'is true when given an RBX style wrong number of arguments message' do
|
14
|
+
is_for! ArgumentError.new "method 'a': given 1, expected 0"
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'is true when given an JRuby style wrong number of arguments message' do
|
18
|
+
is_for! ArgumentError.new "wrong number of arguments calling `a` (1 for 0)"
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'is true when given an MRI style wrong number of arguments message' do
|
22
|
+
is_for! ArgumentError.new "wrong number of arguments (1 for 0)"
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'is false for ArgumentErrors that are not "wrong number of arguments"' do
|
26
|
+
is_not_for! ArgumentError.new "Some other kind of ArgumentError"
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'is false when the message is contained within some other message (not overeager)' do
|
30
|
+
is_not_for! RSpec::Expectations::ExpectationNotMetError.new(<<-MESSAGE)
|
31
|
+
expected: "wrong number of arguments (1 for 0) (ArgumentError)"
|
32
|
+
got: "Wrong number of arguments"
|
33
|
+
|
34
|
+
(compared using ==)
|
35
|
+
MESSAGE
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe 'parse' do
|
40
|
+
let(:mri_message) { "wrong number of arguments (1 for 0)" }
|
41
|
+
let(:mri_parsed) { heuristic_for message: mri_message }
|
42
|
+
|
43
|
+
let(:rbx_message) { "method 'a': given 1, expected 0" }
|
44
|
+
let(:rbx_parsed) { heuristic_for message: rbx_message }
|
45
|
+
|
46
|
+
let(:jruby_message) { "wrong number of arguments calling `a` (1 for 0)" }
|
47
|
+
let(:jruby_parsed) { heuristic_for message: "wrong number of arguments calling `a` (1 for 0)" }
|
48
|
+
|
49
|
+
it 'extracts the number of arguments that were passed' do
|
50
|
+
expect(rbx_parsed.num_expected).to eq 0
|
51
|
+
expect(mri_parsed.num_expected).to eq 0
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'extracts the number of arguments that were received' do
|
55
|
+
expect(rbx_parsed.num_received).to eq 1
|
56
|
+
expect(mri_parsed.num_received).to eq 1
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
let(:message_2_for_3) { "wrong number of arguments calling `a` (1 for 0)" }
|
61
|
+
|
62
|
+
it 'shows the first two lines of the backtrace' do
|
63
|
+
heuristic = heuristic_for message: message_2_for_3, backtrace: [
|
64
|
+
"/a:1:in `a'", "/b:2:in `b'", "/c:3:in `c'"
|
65
|
+
]
|
66
|
+
_heuristic, codeblocks = heuristic.semantic_info
|
67
|
+
paths = codeblocks.map { |_code, attrs| attrs[:location].path.to_s }
|
68
|
+
expect(paths).to eq ['/a', '/b']
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'only shows one code sample when there is only one line in the backtrace, with context before and after, and no message, and highlights its label on the offchance that this is the right thing to do' do
|
72
|
+
heuristic = heuristic_for message: message_2_for_3, backtrace: ["/a:1:in `b'"]
|
73
|
+
_heuristic, *codeblocks = heuristic.semantic_info
|
74
|
+
expect(codeblocks.length).to eq 1
|
75
|
+
((_code, attrs)) = codeblocks
|
76
|
+
expect(attrs).to eq highlight: 'b',
|
77
|
+
context: (-5..5),
|
78
|
+
emphasis: :code,
|
79
|
+
location: ErrorToCommunicate::ExceptionInfo::Location.new(
|
80
|
+
path: '/a', linenum: 1, label: 'b'
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'shrugs "sorry" when there are no lines in the backtrace' do
|
85
|
+
heuristic = heuristic_for message: message_2_for_3, backtrace: []
|
86
|
+
expect(heuristic.semantic_info).to eq [:context, "Couldn\'t find anything interesting ¯\_(ツ)_/¯\n"]
|
87
|
+
end
|
88
|
+
|
89
|
+
describe 'When there are at least two lines in the backtrace' do
|
90
|
+
attr_reader :code1, :code2
|
91
|
+
before :each do
|
92
|
+
heuristic = heuristic_for message: message_2_for_3, backtrace: ["/a:1:in `a'", "/b:2:in `b'", "/b:2:in `b'"]
|
93
|
+
heuristic, ((name1, @code1), (name2, @code2), *rest) = heuristic.semantic_info
|
94
|
+
expect(heuristic).to eq :heuristic
|
95
|
+
expect(name1).to eq :code
|
96
|
+
expect(name2).to eq :code
|
97
|
+
expect(rest).to be_empty
|
98
|
+
end
|
99
|
+
|
100
|
+
describe 'the first line' do
|
101
|
+
it 'has a context of 0..5 (b/c it\'s a method definition, so no point in seeing preceding context)'
|
102
|
+
it 'declares the number of expected args as the message'
|
103
|
+
it 'emphasizes the code'
|
104
|
+
it 'highlights it\'s own label (as it is the method name)'
|
105
|
+
end
|
106
|
+
|
107
|
+
describe 'the second line' do
|
108
|
+
it 'has a context of -5..5 so we can see what we were thinking when we called it'
|
109
|
+
it 'declares the number of sent args as the message'
|
110
|
+
it 'emphasizes the code'
|
111
|
+
it 'highlights the first line\'s label (because that\'s the method call)'
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'error_to_communicate/heuristic'
|
3
|
+
require 'heuristic/spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe 'Heuristic', heuristic: true do
|
6
|
+
let(:einfo) { ErrorToCommunicate::ExceptionInfo.new classname: 'the classname', message: 'the message', backtrace: [
|
7
|
+
ErrorToCommunicate::ExceptionInfo::Location.new(path: 'file', linenum: 12, label: 'a')
|
8
|
+
]
|
9
|
+
}
|
10
|
+
let(:subclass) { Class.new ErrorToCommunicate::Heuristic }
|
11
|
+
let(:instance) { subclass.new einfo: einfo, project: build_default_project }
|
12
|
+
|
13
|
+
it 'expects the subclass to implement .for?' do
|
14
|
+
expect { subclass.for? nil }.to raise_error NotImplementedError, /subclass/
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'records the exception info as einfo' do
|
18
|
+
expect(instance.einfo).to equal einfo
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'delegates classname, and backtrace to einfo' do
|
22
|
+
expect(instance.classname).to eq 'the classname'
|
23
|
+
expect(instance.backtrace.map { |loc| [loc.linenum] }).to eq [[12]]
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'defaults the explanation to einfo\'s message' do
|
27
|
+
expect(instance.explanation).to eq 'the message'
|
28
|
+
end
|
29
|
+
|
30
|
+
describe 'semantic methods' do
|
31
|
+
specify 'semantic_explanation defaults to the explanation' do
|
32
|
+
def instance.explanation; "!#{einfo.message}!"; end
|
33
|
+
expect(instance.semantic_explanation).to eq "!the message!"
|
34
|
+
end
|
35
|
+
|
36
|
+
specify 'semantic_summary includes the classname and semantic_explanation in columns' do
|
37
|
+
def instance.semantic_explanation; 'sem-expl'; end
|
38
|
+
expect(instance.semantic_summary).to eq \
|
39
|
+
[:summary, [
|
40
|
+
[:columns,
|
41
|
+
[:classname, 'the classname'],
|
42
|
+
[:explanation, 'sem-expl']]]]
|
43
|
+
end
|
44
|
+
|
45
|
+
specify 'semantic_info is null by default' do
|
46
|
+
expect(instance.semantic_info).to eq [:null]
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'semantic_backtrace' do
|
50
|
+
it 'is marked as a backtrace' do
|
51
|
+
expect(instance.semantic_backtrace.first).to eq :backtrace
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'includes code for each line of the backtrace, without context, highlighting the label of the predecessor, and emphasizing the path over the code' do
|
55
|
+
err = RuntimeError.new('the message')
|
56
|
+
err.set_backtrace ["file1:12:in `a'", "file2:100:in `b'", "file3:94:in `c'"]
|
57
|
+
einfo = einfo_for err
|
58
|
+
instance = subclass.new einfo: einfo, project: build_default_project
|
59
|
+
code_samples = instance.semantic_backtrace.last
|
60
|
+
metas = code_samples.map do |name, metadata, *rest|
|
61
|
+
expect(name).to eq :code
|
62
|
+
expect(rest).to be_empty
|
63
|
+
metadata
|
64
|
+
end
|
65
|
+
|
66
|
+
locations = metas.map { |m| m[:location] }
|
67
|
+
highlights = metas.map { |m| m[:highlight] }
|
68
|
+
paths_and_lines = locations.flat_map { |l| [l.path.to_s, l.linenum] }
|
69
|
+
expect(paths_and_lines).to eq ['file1', 12, 'file2', 100, 'file3', 94]
|
70
|
+
expect(highlights).to eq ['b', 'c', nil]
|
71
|
+
expect(metas).to be_all { |m| m[:context] == (0..0) }
|
72
|
+
expect(metas).to be_all { |m| m[:emphasis] == :path }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|