reek 1.2.13 → 1.3
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.
- data/CHANGELOG +6 -0
- data/config/defaults.reek +3 -0
- data/features/ruby_api/api.feature +5 -1
- data/features/samples.feature +170 -153
- data/features/step_definitions/reek_steps.rb +1 -9
- data/lib/reek/core/code_parser.rb +4 -4
- data/lib/reek/core/method_context.rb +2 -11
- data/lib/reek/core/smell_repository.rb +1 -0
- data/lib/reek/smells.rb +1 -0
- data/lib/reek/smells/duplication.rb +1 -1
- data/lib/reek/smells/irresponsible_module.rb +5 -1
- data/lib/reek/smells/simulated_polymorphism.rb +1 -1
- data/lib/reek/smells/uncommunicative_variable_name.rb +43 -3
- data/lib/reek/smells/unused_parameters.rb +37 -0
- data/lib/reek/source/source_code.rb +2 -13
- data/lib/reek/source/tree_dresser.rb +11 -24
- data/lib/reek/version.rb +1 -1
- data/reek.gemspec +4 -5
- data/spec/reek/smells/control_couple_spec.rb +1 -5
- data/spec/reek/smells/feature_envy_spec.rb +0 -3
- data/spec/reek/smells/irresponsible_module_spec.rb +10 -1
- data/spec/reek/smells/long_method_spec.rb +24 -2
- data/spec/reek/smells/uncommunicative_variable_name_spec.rb +66 -0
- data/spec/reek/smells/unused_parameters_spec.rb +47 -0
- data/spec/reek/smells/utility_function_spec.rb +3 -3
- data/spec/reek/source/tree_dresser_spec.rb +88 -33
- metadata +40 -40
@@ -9,21 +9,12 @@ module Reek
|
|
9
9
|
#
|
10
10
|
module MethodParameters
|
11
11
|
def default_assignments
|
12
|
-
assignments = self[-1]
|
13
12
|
result = []
|
14
|
-
|
15
|
-
|
16
|
-
result << exp[1..2] if exp[0] == :lasgn
|
13
|
+
self[1..-1].each do |exp|
|
14
|
+
result << exp[1..2] if Sexp === exp && exp[0] == :lasgn
|
17
15
|
end
|
18
16
|
result
|
19
17
|
end
|
20
|
-
def is_arg?(param)
|
21
|
-
return false if is_assignment_block?(param)
|
22
|
-
return !(param.to_s =~ /^\&/)
|
23
|
-
end
|
24
|
-
def is_assignment_block?(param)
|
25
|
-
Array === param and param[0] == :block
|
26
|
-
end
|
27
18
|
end
|
28
19
|
|
29
20
|
#
|
data/lib/reek/smells.rb
CHANGED
@@ -16,6 +16,7 @@ require File.join( File.dirname( File.expand_path(__FILE__)), 'smells', 'uncommu
|
|
16
16
|
require File.join( File.dirname( File.expand_path(__FILE__)), 'smells', 'uncommunicative_module_name')
|
17
17
|
require File.join( File.dirname( File.expand_path(__FILE__)), 'smells', 'uncommunicative_parameter_name')
|
18
18
|
require File.join( File.dirname( File.expand_path(__FILE__)), 'smells', 'uncommunicative_variable_name')
|
19
|
+
require File.join( File.dirname( File.expand_path(__FILE__)), 'smells', 'unused_parameters')
|
19
20
|
require File.join( File.dirname( File.expand_path(__FILE__)), 'smells', 'utility_function')
|
20
21
|
# SMELL: Duplication -- all these should be found automagically
|
21
22
|
|
@@ -79,7 +79,7 @@ module Reek
|
|
79
79
|
result[call_node].push(call_node)
|
80
80
|
end
|
81
81
|
method_ctx.local_nodes(:attrasgn) do |asgn_node|
|
82
|
-
result[asgn_node].push(asgn_node) unless asgn_node.args.
|
82
|
+
result[asgn_node].push(asgn_node) unless asgn_node.args.nil?
|
83
83
|
end
|
84
84
|
result
|
85
85
|
end
|
@@ -19,6 +19,10 @@ module Reek
|
|
19
19
|
[:class]
|
20
20
|
end
|
21
21
|
|
22
|
+
def self.descriptive # :nodoc:
|
23
|
+
@descriptive ||= {}
|
24
|
+
end
|
25
|
+
|
22
26
|
#
|
23
27
|
# Checks the given class or module for a descriptive comment.
|
24
28
|
#
|
@@ -26,7 +30,7 @@ module Reek
|
|
26
30
|
#
|
27
31
|
def examine_context(ctx)
|
28
32
|
comment = Source::CodeComment.new(ctx.exp.comments)
|
29
|
-
return [] if comment.is_descriptive?
|
33
|
+
return [] if self.class.descriptive[ctx.full_name] ||= comment.is_descriptive?
|
30
34
|
smell = SmellWarning.new(SMELL_CLASS, ctx.full_name, [ctx.exp.line],
|
31
35
|
'has no descriptive comment',
|
32
36
|
@source, SMELL_SUBCLASS, {MODULE_NAME_KEY => ctx.exp.text_name})
|
@@ -72,7 +72,7 @@ module Reek
|
|
72
72
|
result = Hash.new {|hash, key| hash[key] = []}
|
73
73
|
collector = proc { |node|
|
74
74
|
condition = node.condition
|
75
|
-
next if condition.nil? or condition == s(:call, nil, :block_given
|
75
|
+
next if condition.nil? or condition == s(:call, nil, :block_given?)
|
76
76
|
result[condition].push(condition.line)
|
77
77
|
}
|
78
78
|
[:if, :case].each {|stmt| sexp.local_nodes(stmt, &collector) }
|
@@ -75,14 +75,54 @@ module Reek
|
|
75
75
|
end
|
76
76
|
|
77
77
|
def variable_names(exp)
|
78
|
+
result = Hash.new {|hash, key| hash[key] = []}
|
79
|
+
find_assignment_variable_names(exp, result)
|
80
|
+
find_block_argument_variable_names(exp, result)
|
81
|
+
result
|
82
|
+
end
|
83
|
+
|
84
|
+
def find_assignment_variable_names(exp, accumulator)
|
78
85
|
assignment_nodes = exp.each_node(:lasgn, [:class, :module, :defs, :defn])
|
86
|
+
|
79
87
|
case exp.first
|
80
88
|
when :class, :module
|
81
89
|
assignment_nodes += exp.each_node(:iasgn, [:class, :module])
|
82
90
|
end
|
83
|
-
|
84
|
-
assignment_nodes.each {|asgn|
|
85
|
-
|
91
|
+
|
92
|
+
assignment_nodes.each {|asgn| accumulator[asgn[1]].push(asgn.line) }
|
93
|
+
end
|
94
|
+
|
95
|
+
def find_block_argument_variable_names(exp, accumulator)
|
96
|
+
arg_search_exp = case exp.first
|
97
|
+
when :class, :module
|
98
|
+
exp
|
99
|
+
when :defs, :defn
|
100
|
+
exp.body
|
101
|
+
end
|
102
|
+
|
103
|
+
args_nodes = arg_search_exp.each_node(:args, [:class, :module, :defs, :defn])
|
104
|
+
|
105
|
+
args_nodes.each do |args_node|
|
106
|
+
recursively_record_variable_names(accumulator, args_node)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def recursively_record_variable_names(accumulator, exp)
|
111
|
+
exp[1..-1].each do |subexp|
|
112
|
+
if subexp.is_a? Symbol
|
113
|
+
record_variable_name(exp, subexp, accumulator)
|
114
|
+
elsif subexp.first == :masgn
|
115
|
+
recursively_record_variable_names(accumulator, subexp)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def record_variable_name(exp, symbol, accumulator)
|
121
|
+
varname = symbol.to_s.sub(/^\*/, '')
|
122
|
+
if varname != ""
|
123
|
+
var = varname.to_sym
|
124
|
+
accumulator[var].push(exp.line)
|
125
|
+
end
|
86
126
|
end
|
87
127
|
end
|
88
128
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require File.join( File.dirname( File.expand_path(__FILE__)), 'smell_detector')
|
2
|
+
require File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'smell_warning')
|
3
|
+
|
4
|
+
module Reek
|
5
|
+
module Smells
|
6
|
+
|
7
|
+
#
|
8
|
+
# Methods should use their parameters.
|
9
|
+
#
|
10
|
+
class UnusedParameters < SmellDetector
|
11
|
+
|
12
|
+
SMELL_CLASS = 'ControlCouple'
|
13
|
+
SMELL_SUBCLASS = name.split(/::/)[-1]
|
14
|
+
|
15
|
+
PARAMETER_KEY = 'parameter'
|
16
|
+
|
17
|
+
#
|
18
|
+
# Checks whether the given method has any unused parameters.
|
19
|
+
#
|
20
|
+
# @return [Array<SmellWarning>]
|
21
|
+
#
|
22
|
+
def examine_context(method_ctx)
|
23
|
+
params = method_ctx.exp.arg_names || []
|
24
|
+
params.select do |param|
|
25
|
+
param = param.to_s.sub(/^\*/, '')
|
26
|
+
!["", "_"].include?(param) &&
|
27
|
+
!method_ctx.local_nodes(:lvar).include?(Sexp.new(:lvar, param.to_sym))
|
28
|
+
end.map do |param|
|
29
|
+
SmellWarning.new(SMELL_CLASS, method_ctx.full_name, [method_ctx.exp.line],
|
30
|
+
"has unused parameter '#{param.to_s}'",
|
31
|
+
@source, SMELL_SUBCLASS, {PARAMETER_KEY => param.to_s})
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'ruby_parser'
|
1
2
|
require File.join(File.dirname(File.expand_path(__FILE__)), 'config_file')
|
2
3
|
require File.join(File.dirname(File.expand_path(__FILE__)), 'tree_dresser')
|
3
4
|
|
@@ -21,19 +22,7 @@ module Reek
|
|
21
22
|
|
22
23
|
attr_reader :desc
|
23
24
|
|
24
|
-
|
25
|
-
# reek uses that parser and will be able to handle Ruby 1.9 syntax. On
|
26
|
-
# Ruby versions below 1.9.3, it will fail and reek will use ruby_parser
|
27
|
-
# and handle Ruby 1.8 syntax only.
|
28
|
-
PARSER_CLASS = begin
|
29
|
-
require 'ripper_ruby_parser'
|
30
|
-
RipperRubyParser::Parser
|
31
|
-
rescue LoadError
|
32
|
-
require 'ruby_parser'
|
33
|
-
RubyParser
|
34
|
-
end
|
35
|
-
|
36
|
-
def initialize(code, desc, parser = PARSER_CLASS.new)
|
25
|
+
def initialize(code, desc, parser = RubyParser.new)
|
37
26
|
@source = code
|
38
27
|
@desc = desc
|
39
28
|
@parser = parser
|
@@ -69,9 +69,9 @@ module Reek
|
|
69
69
|
module CallNode
|
70
70
|
def receiver() self[1] end
|
71
71
|
def method_name() self[2] end
|
72
|
-
def args() self[3] end
|
72
|
+
def args() self[3..-1] end
|
73
73
|
def arg_names
|
74
|
-
args
|
74
|
+
args.map {|arg| arg[1]}
|
75
75
|
end
|
76
76
|
end
|
77
77
|
|
@@ -84,26 +84,19 @@ module Reek
|
|
84
84
|
|
85
85
|
module MethodNode
|
86
86
|
def arg_names
|
87
|
-
|
88
|
-
@args = argslist[1..-1].reject {|param| Sexp === param or param.to_s =~ /^&/}
|
89
|
-
end
|
90
|
-
@args
|
91
|
-
end
|
92
|
-
def parameters()
|
93
|
-
unless @params
|
94
|
-
@params = argslist.reject {|param| Sexp === param}
|
95
|
-
end
|
96
|
-
@params
|
87
|
+
@args ||= parameter_names.reject {|param| param.to_s =~ /^&/}
|
97
88
|
end
|
98
89
|
def parameter_names
|
99
|
-
|
90
|
+
@param_names ||= argslist[1..-1].map { |param| Sexp === param ? param[1] : param }
|
100
91
|
end
|
101
92
|
end
|
102
93
|
|
103
94
|
module DefnNode
|
104
95
|
def name() self[1] end
|
105
96
|
def argslist() self[2] end
|
106
|
-
def body()
|
97
|
+
def body()
|
98
|
+
self[3..-1].tap {|b| b.extend SexpNode }
|
99
|
+
end
|
107
100
|
include MethodNode
|
108
101
|
def full_name(outer)
|
109
102
|
prefix = outer == '' ? '' : "#{outer}#"
|
@@ -115,7 +108,9 @@ module Reek
|
|
115
108
|
def receiver() self[1] end
|
116
109
|
def name() self[2] end
|
117
110
|
def argslist() self[3] end
|
118
|
-
def body()
|
111
|
+
def body()
|
112
|
+
self[4..-1].tap {|b| b.extend SexpNode }
|
113
|
+
end
|
119
114
|
include MethodNode
|
120
115
|
def full_name(outer)
|
121
116
|
prefix = outer == '' ? '' : "#{outer}#"
|
@@ -133,15 +128,7 @@ module Reek
|
|
133
128
|
def block() self[3] end
|
134
129
|
def parameters() self[2] || [] end
|
135
130
|
def parameter_names
|
136
|
-
|
137
|
-
return case result[0]
|
138
|
-
when :lasgn
|
139
|
-
[result[1]]
|
140
|
-
when :masgn
|
141
|
-
result[1][1..-1].map {|lasgn| lasgn[1]}
|
142
|
-
else
|
143
|
-
[]
|
144
|
-
end
|
131
|
+
parameters[1..-1].to_a
|
145
132
|
end
|
146
133
|
end
|
147
134
|
|
data/lib/reek/version.rb
CHANGED
data/reek.gemspec
CHANGED
@@ -6,7 +6,6 @@ Gem::Specification.new do |s|
|
|
6
6
|
s.version = Reek::VERSION
|
7
7
|
|
8
8
|
s.authors = ['Kevin Rutherford', 'Timo Roessner', 'Matijs van Zuijlen']
|
9
|
-
s.date = %q{2010-04-26}
|
10
9
|
s.default_executable = %q{reek}
|
11
10
|
s.description = %q{Reek is a tool that examines Ruby classes, modules and methods
|
12
11
|
and reports any code smells it finds.
|
@@ -26,13 +25,13 @@ and reports any code smells it finds.
|
|
26
25
|
s.rubygems_version = %q{1.3.6}
|
27
26
|
s.summary = %q{Code smell detector for Ruby}
|
28
27
|
|
29
|
-
s.add_runtime_dependency(%q<ruby_parser>, ["~>
|
30
|
-
s.add_runtime_dependency(%q<
|
31
|
-
s.add_runtime_dependency(%q<ruby2ruby>, ["~>
|
32
|
-
s.add_runtime_dependency(%q<sexp_processor>, ["~> 3.0"])
|
28
|
+
s.add_runtime_dependency(%q<ruby_parser>, ["~> 3.0.4"])
|
29
|
+
s.add_runtime_dependency(%q<sexp_processor>)
|
30
|
+
s.add_runtime_dependency(%q<ruby2ruby>, ["~> 2.0.0"])
|
33
31
|
|
34
32
|
s.add_development_dependency(%q<bundler>, ["~> 1.1"])
|
35
33
|
s.add_development_dependency(%q<rake>)
|
36
34
|
s.add_development_dependency(%q<cucumber>)
|
37
35
|
s.add_development_dependency(%q<rspec>, ["~> 2.12"])
|
36
|
+
s.add_development_dependency(%q<yard>)
|
38
37
|
end
|
@@ -51,11 +51,7 @@ EOS
|
|
51
51
|
|
52
52
|
it 'has the correct fields' do
|
53
53
|
@warning.smell[ControlCouple::PARAMETER_KEY].should == 'arg'
|
54
|
-
|
55
|
-
@warning.lines.should == [3,6]
|
56
|
-
else
|
57
|
-
@warning.lines.should == [3,5]
|
58
|
-
end
|
54
|
+
@warning.lines.should == [3, 5]
|
59
55
|
end
|
60
56
|
end
|
61
57
|
end
|
@@ -13,9 +13,6 @@ describe FeatureEnvy do
|
|
13
13
|
it 'should not report vcall with no argument' do
|
14
14
|
'def simple() func; end'.should_not reek
|
15
15
|
end
|
16
|
-
it 'should not report vcall with argument' do
|
17
|
-
'def simple(arga) func(17); end'.should_not reek
|
18
|
-
end
|
19
16
|
it 'should not report single use' do
|
20
17
|
'def no_envy(arga) arga.barg(@item) end'.should_not reek
|
21
18
|
end
|
@@ -1,7 +1,6 @@
|
|
1
1
|
require File.join(File.dirname(File.dirname(File.dirname(File.expand_path(__FILE__)))), 'spec_helper')
|
2
2
|
require File.join(File.dirname(File.dirname(File.dirname(File.dirname(File.expand_path(__FILE__))))), 'lib', 'reek', 'smells', 'irresponsible_module')
|
3
3
|
require File.join(File.dirname(File.expand_path(__FILE__)), 'smell_detector_shared')
|
4
|
-
|
5
4
|
include Reek::Smells
|
6
5
|
|
7
6
|
describe IrresponsibleModule do
|
@@ -12,6 +11,16 @@ describe IrresponsibleModule do
|
|
12
11
|
|
13
12
|
it_should_behave_like 'SmellDetector'
|
14
13
|
|
14
|
+
it 'does not report re-opened modules' do
|
15
|
+
src = <<-EOS
|
16
|
+
# Abstract base class
|
17
|
+
class C; end
|
18
|
+
|
19
|
+
class C; def foo; end; end
|
20
|
+
EOS
|
21
|
+
src.should_not reek_of(:IrresponsibleModule)
|
22
|
+
end
|
23
|
+
|
15
24
|
it "does not report a class having a comment" do
|
16
25
|
src = <<EOS
|
17
26
|
# test class
|
@@ -13,6 +13,12 @@ def process_method(src)
|
|
13
13
|
Core::CodeParser.new(sniffer).process_defn(source.syntax_tree)
|
14
14
|
end
|
15
15
|
|
16
|
+
def process_singleton_method(src)
|
17
|
+
source = src.to_reek_source
|
18
|
+
sniffer = Core::Sniffer.new(source)
|
19
|
+
Core::CodeParser.new(sniffer).process_defs(source.syntax_tree)
|
20
|
+
end
|
21
|
+
|
16
22
|
describe LongMethod do
|
17
23
|
it 'should not report short methods' do
|
18
24
|
src = 'def short(arga) alf = f(1);@bet = 2;@cut = 3;@dit = 4; @emp = 5;end'
|
@@ -20,7 +26,7 @@ describe LongMethod do
|
|
20
26
|
end
|
21
27
|
|
22
28
|
it 'should report long methods' do
|
23
|
-
src = 'def long(
|
29
|
+
src = 'def long() alf = f(1);@bet = 2;@cut = 3;@dit = 4; @emp = 5;@fry = 6;end'
|
24
30
|
src.should reek_only_of(:LongMethod, /6 statements/)
|
25
31
|
end
|
26
32
|
|
@@ -53,7 +59,7 @@ EOS
|
|
53
59
|
|
54
60
|
it 'should report long inner block' do
|
55
61
|
src = <<EOS
|
56
|
-
def long(
|
62
|
+
def long()
|
57
63
|
f(3)
|
58
64
|
self.each do |xyzero|
|
59
65
|
xyzero = 1
|
@@ -177,6 +183,22 @@ describe LongMethod, 'does not count control statements' do
|
|
177
183
|
method.num_statements.should == 0
|
178
184
|
end
|
179
185
|
|
186
|
+
it 'counts 1 statement in a block' do
|
187
|
+
method = process_method('def one() fred.each do; callee(); end; end')
|
188
|
+
method.num_statements.should == 1
|
189
|
+
end
|
190
|
+
|
191
|
+
# FIXME: I think this is wrong, but it specs current behavior.
|
192
|
+
it 'counts 4 statements in a block' do
|
193
|
+
method = process_method('def one() fred.each do; callee(); callee(); callee(); end; end')
|
194
|
+
method.num_statements.should == 4
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'counts 1 statement in a singleton method' do
|
198
|
+
method = process_singleton_method('def self.foo; callee(); end')
|
199
|
+
method.num_statements.should == 1
|
200
|
+
end
|
201
|
+
|
180
202
|
it 'counts else statement' do
|
181
203
|
src = <<EOS
|
182
204
|
def parse(arg, argv, &error)
|
@@ -80,6 +80,72 @@ EOS
|
|
80
80
|
src.should smell_of(UncommunicativeVariableName,
|
81
81
|
{UncommunicativeVariableName::VARIABLE_NAME_KEY => 'x'})
|
82
82
|
end
|
83
|
+
|
84
|
+
it "reports all relevant block parameters" do
|
85
|
+
src = <<-EOS
|
86
|
+
def bad
|
87
|
+
@foo.map { |x, y| x + y }
|
88
|
+
end
|
89
|
+
EOS
|
90
|
+
src.should smell_of(UncommunicativeVariableName,
|
91
|
+
{UncommunicativeVariableName::VARIABLE_NAME_KEY => 'x'},
|
92
|
+
{UncommunicativeVariableName::VARIABLE_NAME_KEY => 'y'})
|
93
|
+
end
|
94
|
+
|
95
|
+
it "reports block parameters used outside of methods" do
|
96
|
+
src = <<-EOS
|
97
|
+
class Foo
|
98
|
+
@foo.map { |x| x * 2 }
|
99
|
+
end
|
100
|
+
EOS
|
101
|
+
src.should smell_of(UncommunicativeVariableName,
|
102
|
+
{UncommunicativeVariableName::VARIABLE_NAME_KEY => 'x'})
|
103
|
+
end
|
104
|
+
|
105
|
+
it "reports splatted block parameters correctly" do
|
106
|
+
src = <<-EOS
|
107
|
+
def bad
|
108
|
+
@foo.map { |*y| y << 1 }
|
109
|
+
end
|
110
|
+
EOS
|
111
|
+
src.should smell_of(UncommunicativeVariableName,
|
112
|
+
{UncommunicativeVariableName::VARIABLE_NAME_KEY => 'y'})
|
113
|
+
end
|
114
|
+
|
115
|
+
it "reports nested block parameters" do
|
116
|
+
src = <<-EOS
|
117
|
+
def bad
|
118
|
+
@foo.map { |(x, y)| x + y }
|
119
|
+
end
|
120
|
+
EOS
|
121
|
+
src.should smell_of(UncommunicativeVariableName,
|
122
|
+
{UncommunicativeVariableName::VARIABLE_NAME_KEY => 'x'},
|
123
|
+
{UncommunicativeVariableName::VARIABLE_NAME_KEY => 'y'})
|
124
|
+
end
|
125
|
+
|
126
|
+
it "reports splatted nested block parameters" do
|
127
|
+
src = <<-EOS
|
128
|
+
def bad
|
129
|
+
@foo.map { |(x, *y)| x + y }
|
130
|
+
end
|
131
|
+
EOS
|
132
|
+
src.should smell_of(UncommunicativeVariableName,
|
133
|
+
{UncommunicativeVariableName::VARIABLE_NAME_KEY => 'x'},
|
134
|
+
{UncommunicativeVariableName::VARIABLE_NAME_KEY => 'y'})
|
135
|
+
end
|
136
|
+
|
137
|
+
it "reports deeply nested block parameters" do
|
138
|
+
src = <<-EOS
|
139
|
+
def bad
|
140
|
+
@foo.map { |(x, (y, z))| x + y + z }
|
141
|
+
end
|
142
|
+
EOS
|
143
|
+
src.should smell_of(UncommunicativeVariableName,
|
144
|
+
{UncommunicativeVariableName::VARIABLE_NAME_KEY => 'x'},
|
145
|
+
{UncommunicativeVariableName::VARIABLE_NAME_KEY => 'y'},
|
146
|
+
{UncommunicativeVariableName::VARIABLE_NAME_KEY => 'z'})
|
147
|
+
end
|
148
|
+
|
83
149
|
end
|
84
150
|
|
85
151
|
context 'when a smell is reported' do
|