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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/Readme.md +9 -7
  4. data/experiments/formatting/other_resources +7 -0
  5. data/lib/error_to_communicate/at_exit.rb +15 -9
  6. data/lib/error_to_communicate/config.rb +55 -22
  7. data/lib/error_to_communicate/exception_info.rb +86 -31
  8. data/lib/error_to_communicate/format_terminal.rb +146 -0
  9. data/lib/error_to_communicate/heuristic/exception.rb +22 -0
  10. data/lib/error_to_communicate/heuristic/load_error.rb +57 -0
  11. data/lib/error_to_communicate/heuristic/no_method_error.rb +35 -0
  12. data/lib/error_to_communicate/heuristic/syntax_error.rb +55 -0
  13. data/lib/error_to_communicate/heuristic/wrong_number_of_arguments.rb +73 -0
  14. data/lib/error_to_communicate/heuristic.rb +54 -0
  15. data/lib/error_to_communicate/project.rb +50 -0
  16. data/lib/error_to_communicate/rspec_formatter.rb +8 -9
  17. data/lib/error_to_communicate/theme.rb +137 -0
  18. data/lib/error_to_communicate/version.rb +2 -2
  19. data/lib/error_to_communicate.rb +4 -2
  20. data/spec/acceptance/exception_spec.rb +2 -4
  21. data/spec/acceptance/load_error_spec.rb +23 -0
  22. data/spec/acceptance/name_error_spec.rb +46 -0
  23. data/spec/acceptance/no_methood_error_spec.rb +6 -8
  24. data/spec/acceptance/runtime_error_spec.rb +27 -0
  25. data/spec/acceptance/short_and_long_require_spec.rb +29 -0
  26. data/spec/acceptance/spec_helper.rb +4 -3
  27. data/spec/acceptance/syntax_error_spec.rb +32 -0
  28. data/spec/acceptance/{argument_error_spec.rb → wrong_number_of_arguments_spec.rb} +1 -1
  29. data/spec/config_spec.rb +120 -0
  30. data/spec/heuristic/exception_spec.rb +17 -0
  31. data/spec/heuristic/load_error_spec.rb +195 -0
  32. data/spec/heuristic/no_method_error_spec.rb +25 -0
  33. data/spec/heuristic/spec_helper.rb +33 -0
  34. data/spec/heuristic/wrong_number_of_arguments_spec.rb +115 -0
  35. data/spec/heuristic_spec.rb +76 -0
  36. data/spec/parsing_exception_info_spec.rb +212 -0
  37. data/spec/rspec_formatter_spec.rb +3 -1
  38. data/spec/spec_helper.rb +28 -1
  39. data/what_weve_got_here_is_an_error_to_communicate.gemspec +2 -2
  40. metadata +29 -19
  41. data/lib/error_to_communicate/format/terminal_helpers.rb +0 -97
  42. data/lib/error_to_communicate/format.rb +0 -132
  43. data/lib/error_to_communicate/parse/backtrace.rb +0 -34
  44. data/lib/error_to_communicate/parse/exception.rb +0 -21
  45. data/lib/error_to_communicate/parse/no_method_error.rb +0 -27
  46. data/lib/error_to_communicate/parse/registry.rb +0 -30
  47. data/lib/error_to_communicate/parse/wrong_number_of_arguments.rb +0 -35
  48. data/spec/parse/backtrace_spec.rb +0 -101
  49. data/spec/parse/exception_spec.rb +0 -14
  50. data/spec/parse/no_method_error_spec.rb +0 -23
  51. data/spec/parse/registered_parsers_spec.rb +0 -68
  52. data/spec/parse/spec_helper.rb +0 -23
  53. data/spec/parse/wrong_number_of_arguments_spec.rb +0 -77
@@ -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