bullematic 0.1.0

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.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +12 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +180 -0
  5. data/Rakefile +26 -0
  6. data/Steepfile +15 -0
  7. data/assets/logo-header.svg +28 -0
  8. data/lib/bullematic/ast/finder.rb +175 -0
  9. data/lib/bullematic/ast/parser.rb +58 -0
  10. data/lib/bullematic/ast/rewriter.rb +153 -0
  11. data/lib/bullematic/configuration.rb +66 -0
  12. data/lib/bullematic/detection.rb +109 -0
  13. data/lib/bullematic/fixer.rb +110 -0
  14. data/lib/bullematic/integrations/minitest.rb +37 -0
  15. data/lib/bullematic/integrations/rails.rb +40 -0
  16. data/lib/bullematic/integrations/rspec.rb +33 -0
  17. data/lib/bullematic/logger.rb +103 -0
  18. data/lib/bullematic/notifier.rb +76 -0
  19. data/lib/bullematic/version.rb +6 -0
  20. data/lib/bullematic.rb +54 -0
  21. data/rbs_collection.lock.yaml +24 -0
  22. data/rbs_collection.yaml +14 -0
  23. data/sig/generated/bullematic/ast/finder.rbs +91 -0
  24. data/sig/generated/bullematic/ast/parser.rbs +39 -0
  25. data/sig/generated/bullematic/ast/rewriter.rbs +77 -0
  26. data/sig/generated/bullematic/configuration.rbs +135 -0
  27. data/sig/generated/bullematic/detection.rbs +117 -0
  28. data/sig/generated/bullematic/fixer.rbs +33 -0
  29. data/sig/generated/bullematic/integrations/minitest.rbs +18 -0
  30. data/sig/generated/bullematic/integrations/rails.rbs +20 -0
  31. data/sig/generated/bullematic/integrations/rspec.rbs +10 -0
  32. data/sig/generated/bullematic/logger.rbs +61 -0
  33. data/sig/generated/bullematic/notifier.rbs +30 -0
  34. data/sig/generated/bullematic/version.rbs +5 -0
  35. data/sig/generated/bullematic.rbs +27 -0
  36. data/sig/stubs/rails.rbs +87 -0
  37. metadata +108 -0
@@ -0,0 +1,175 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require "set"
5
+
6
+ module Bullematic
7
+ module AST
8
+ class Finder
9
+ QUERY_METHODS = %i[all where find find_by first last order limit offset].freeze #: Array[Symbol]
10
+
11
+ # @rbs!
12
+ # type query_location = { node: untyped, location: untyped, receiver: untyped, method_name: Symbol }
13
+
14
+ QueryLocation = Struct.new(:node, :location, :receiver, :method_name, keyword_init: true)
15
+
16
+ # @rbs @parse_result: untyped
17
+
18
+ # @rbs!
19
+ # attr_reader parse_result: untyped
20
+ attr_reader :parse_result
21
+
22
+ # @rbs parse_result: untyped
23
+ # @rbs return: void
24
+ def initialize(parse_result)
25
+ @parse_result = parse_result
26
+ end
27
+
28
+ # @rbs model_class_name: String
29
+ # @rbs line_number: Integer?
30
+ # @rbs return: Array[QueryLocation]
31
+ def find_model_queries(model_class_name, line_number: nil)
32
+ queries = [] #: Array[QueryLocation]
33
+ visit_node(parse_result.value, queries, model_class_name, line_number)
34
+ queries
35
+ end
36
+
37
+ # @rbs line_number: Integer
38
+ # @rbs return: QueryLocation?
39
+ def find_query_at_line(line_number)
40
+ queries = [] #: Array[QueryLocation]
41
+ visit_all_queries(parse_result.value, queries, line_number)
42
+ queries.first
43
+ end
44
+
45
+ private
46
+
47
+ # @rbs node: untyped
48
+ # @rbs queries: Array[QueryLocation]
49
+ # @rbs model_class_name: String
50
+ # @rbs target_line: Integer?
51
+ # @rbs skip_nodes: Set[Integer]
52
+ # @rbs return: void
53
+ def visit_node(node, queries, model_class_name, target_line, skip_nodes = Set.new)
54
+ return unless node.respond_to?(:child_nodes)
55
+ return if skip_nodes.include?(node.object_id)
56
+
57
+ case node
58
+ when Prism::InstanceVariableWriteNode
59
+ if check_assignment_node(node, queries, model_class_name, target_line)
60
+ mark_descendant_calls(node.value, skip_nodes)
61
+ end
62
+ when Prism::CallNode
63
+ check_call_node(node, queries, model_class_name, target_line)
64
+ end
65
+
66
+ node.child_nodes.compact.each do |child|
67
+ visit_node(child, queries, model_class_name, target_line, skip_nodes)
68
+ end
69
+ end
70
+
71
+ # @rbs node: untyped
72
+ # @rbs skip_nodes: Set[Integer]
73
+ # @rbs return: void
74
+ def mark_descendant_calls(node, skip_nodes)
75
+ return unless node.respond_to?(:child_nodes)
76
+
77
+ skip_nodes.add(node.object_id) if node.is_a?(Prism::CallNode)
78
+
79
+ node.child_nodes.compact.each do |child|
80
+ mark_descendant_calls(child, skip_nodes)
81
+ end
82
+ end
83
+
84
+ # @rbs node: untyped
85
+ # @rbs queries: Array[QueryLocation]
86
+ # @rbs target_line: Integer?
87
+ # @rbs return: void
88
+ def visit_all_queries(node, queries, target_line)
89
+ return unless node.respond_to?(:child_nodes)
90
+
91
+ if node.is_a?(Prism::CallNode) && query_method?(node.name)
92
+ node_line = node.location.start_line
93
+ if target_line.nil? || node_line == target_line
94
+ queries << QueryLocation.new(
95
+ node: node,
96
+ location: node.location,
97
+ receiver: find_root_receiver(node),
98
+ method_name: node.name
99
+ )
100
+ end
101
+ end
102
+
103
+ node.child_nodes.compact.each do |child|
104
+ visit_all_queries(child, queries, target_line)
105
+ end
106
+ end
107
+
108
+ # @rbs node: untyped
109
+ # @rbs queries: Array[QueryLocation]
110
+ # @rbs model_class_name: String
111
+ # @rbs target_line: Integer?
112
+ # @rbs return: void
113
+ def check_call_node(node, queries, model_class_name, target_line)
114
+ return unless query_method?(node.name)
115
+ return if target_line && node.location.start_line != target_line
116
+
117
+ root_receiver = find_root_receiver(node)
118
+ return unless model_constant?(root_receiver, model_class_name)
119
+
120
+ queries << QueryLocation.new(
121
+ node: node,
122
+ location: node.location,
123
+ receiver: root_receiver,
124
+ method_name: node.name
125
+ )
126
+ end
127
+
128
+ # @rbs node: untyped
129
+ # @rbs queries: Array[QueryLocation]
130
+ # @rbs model_class_name: String
131
+ # @rbs target_line: Integer?
132
+ # @rbs return: bool
133
+ def check_assignment_node(node, queries, model_class_name, target_line)
134
+ return false if target_line && node.location.start_line != target_line
135
+
136
+ value = node.value
137
+ return false unless value.is_a?(Prism::CallNode)
138
+
139
+ root_receiver = find_root_receiver(value)
140
+ return false unless model_constant?(root_receiver, model_class_name)
141
+
142
+ queries << QueryLocation.new(
143
+ node: value,
144
+ location: value.location,
145
+ receiver: root_receiver,
146
+ method_name: value.name
147
+ )
148
+ true
149
+ end
150
+
151
+ # @rbs node: untyped
152
+ # @rbs return: untyped
153
+ def find_root_receiver(node)
154
+ current = node
155
+ current = current.receiver while current.is_a?(Prism::CallNode) && current.receiver
156
+ current
157
+ end
158
+
159
+ # @rbs node: untyped
160
+ # @rbs model_class_name: String
161
+ # @rbs return: bool
162
+ def model_constant?(node, model_class_name)
163
+ return false unless node.is_a?(Prism::ConstantReadNode)
164
+
165
+ node.name.to_s == model_class_name
166
+ end
167
+
168
+ # @rbs method_name: Symbol
169
+ # @rbs return: bool
170
+ def query_method?(method_name)
171
+ QUERY_METHODS.include?(method_name)
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,58 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require "prism"
5
+
6
+ module Bullematic
7
+ module AST
8
+ class Parser
9
+ # @rbs @source_code: String
10
+ # @rbs @filepath: String?
11
+
12
+ class << self
13
+ #: () -> Hash[String, untyped]
14
+ def cache
15
+ @cache ||= {} #: Hash[String, untyped]
16
+ end
17
+
18
+ #: () -> void
19
+ def clear_cache
20
+ @cache = {} #: Hash[String, untyped]
21
+ end
22
+
23
+ # @rbs filepath: String
24
+ # @rbs return: untyped
25
+ def parse_file(filepath)
26
+ return cache[filepath] if cache.key?(filepath)
27
+
28
+ result = Prism.parse_file(filepath)
29
+ raise ParseError, result.errors.map(&:message).join("\n") if result.failure?
30
+
31
+ cache[filepath] = result
32
+ result
33
+ end
34
+ end
35
+
36
+ # @rbs!
37
+ # attr_reader source_code: String
38
+ # attr_reader filepath: String?
39
+ attr_reader :source_code, :filepath
40
+
41
+ # @rbs source_code: String
42
+ # @rbs filepath: String?
43
+ # @rbs return: void
44
+ def initialize(source_code, filepath: nil)
45
+ @source_code = source_code
46
+ @filepath = filepath
47
+ end
48
+
49
+ #: () -> untyped
50
+ def parse
51
+ result = Prism.parse(@source_code, filepath: @filepath)
52
+ raise ParseError, result.errors.map(&:message).join("\n") if result.failure?
53
+
54
+ result
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,153 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Bullematic
5
+ module AST
6
+ class Rewriter
7
+ # @rbs!
8
+ # type modification = { type: Symbol, offset: Integer, byte_length: Integer, new_text: String, associations: Array[Symbol] }
9
+
10
+ Modification = Struct.new(:type, :offset, :byte_length, :new_text, :associations, keyword_init: true)
11
+
12
+ # @rbs @source: String
13
+ # @rbs @modifications: Array[Modification]
14
+
15
+ # @rbs!
16
+ # attr_reader source: String
17
+ # attr_reader modifications: Array[Modification]
18
+ attr_reader :source, :modifications
19
+
20
+ # @rbs source: String
21
+ # @rbs return: void
22
+ def initialize(source)
23
+ @source = source.dup
24
+ @modifications = []
25
+ end
26
+
27
+ # @rbs query_location: Finder::QueryLocation
28
+ # @rbs associations: Array[Symbol]
29
+ # @rbs return: void
30
+ def add_includes(query_location, associations)
31
+ return if already_has_includes?(query_location, associations)
32
+
33
+ strategy = Bullematic.configuration&.fix_strategy || :includes
34
+ insert_point = find_insert_point(query_location)
35
+
36
+ assoc_string = format_associations(associations)
37
+ new_text = ".#{strategy}(#{assoc_string})"
38
+
39
+ @modifications << Modification.new(
40
+ type: :insert,
41
+ offset: insert_point,
42
+ byte_length: 0,
43
+ new_text: new_text,
44
+ associations: associations
45
+ )
46
+ end
47
+
48
+ #: () -> String
49
+ def rewrite
50
+ return @source if @modifications.empty?
51
+
52
+ sorted = @modifications.sort_by { |m| -m.offset }
53
+
54
+ result = @source.dup
55
+ sorted.each do |mod|
56
+ case mod.type
57
+ when :insert
58
+ result.insert(mod.offset, mod.new_text)
59
+ when :replace
60
+ result[mod.offset, mod.byte_length] = mod.new_text
61
+ when :delete
62
+ result[mod.offset, mod.byte_length] = ""
63
+ end
64
+ end
65
+
66
+ result
67
+ end
68
+
69
+ private
70
+
71
+ # @rbs query_location: Finder::QueryLocation
72
+ # @rbs return: Integer
73
+ def find_insert_point(query_location)
74
+ node = query_location.node
75
+ receiver = query_location.receiver
76
+
77
+ if receiver.is_a?(Prism::ConstantReadNode)
78
+ receiver.location.end_offset
79
+ else
80
+ find_chain_insert_point(node)
81
+ end
82
+ end
83
+
84
+ # @rbs node: untyped
85
+ # @rbs return: Integer
86
+ def find_chain_insert_point(node)
87
+ current = node
88
+ current = current.receiver while current.is_a?(Prism::CallNode) && current.receiver.is_a?(Prism::CallNode)
89
+
90
+ if current.receiver
91
+ current.receiver.location.end_offset
92
+ else
93
+ current.location.start_offset
94
+ end
95
+ end
96
+
97
+ # @rbs query_location: Finder::QueryLocation
98
+ # @rbs associations: Array[Symbol]
99
+ # @rbs return: bool
100
+ def already_has_includes?(query_location, associations)
101
+ existing = find_existing_includes(query_location.node)
102
+ return false if existing.empty?
103
+
104
+ associations.all? { |assoc| existing.include?(assoc) }
105
+ end
106
+
107
+ # @rbs node: untyped
108
+ # @rbs return: Array[Symbol]
109
+ def find_existing_includes(node)
110
+ includes = [] #: Array[Symbol]
111
+ current = node
112
+
113
+ while current.is_a?(Prism::CallNode)
114
+ if %i[includes preload eager_load].include?(current.name)
115
+ includes.concat(extract_associations_from_call(current))
116
+ end
117
+ current = current.receiver
118
+ end
119
+
120
+ includes
121
+ end
122
+
123
+ # @rbs call_node: untyped
124
+ # @rbs return: Array[Symbol]
125
+ def extract_associations_from_call(call_node)
126
+ return [] unless call_node.arguments
127
+
128
+ associations = [] #: Array[Symbol]
129
+ call_node.arguments.arguments.each do |arg|
130
+ case arg
131
+ when Prism::SymbolNode
132
+ associations << arg.value.to_sym
133
+ when Prism::ArrayNode
134
+ arg.elements.each do |elem|
135
+ associations << elem.value.to_sym if elem.is_a?(Prism::SymbolNode)
136
+ end
137
+ end
138
+ end
139
+ associations
140
+ end
141
+
142
+ # @rbs associations: Array[Symbol]
143
+ # @rbs return: String
144
+ def format_associations(associations)
145
+ if associations.size == 1
146
+ ":#{associations.first}"
147
+ else
148
+ associations.map { |a| ":#{a}" }.join(", ")
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,66 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require "logger"
5
+
6
+ module Bullematic
7
+ class Configuration
8
+ # @rbs skip: to avoid empty array warning
9
+ DEFAULTS = { #: Hash[Symbol, untyped]
10
+ enabled: true,
11
+ auto_fix: true,
12
+ target_paths: %w[app/controllers app/models app/services],
13
+ skip_paths: [],
14
+ dry_run: false,
15
+ backup: false,
16
+ fix_strategy: :includes,
17
+ logger: nil,
18
+ debug: false
19
+ }.freeze
20
+
21
+ # @rbs @enabled: bool
22
+ # @rbs @auto_fix: bool
23
+ # @rbs @target_paths: Array[String]
24
+ # @rbs @skip_paths: Array[String]
25
+ # @rbs @dry_run: bool
26
+ # @rbs @backup: bool
27
+ # @rbs @fix_strategy: Symbol
28
+ # @rbs @debug: bool
29
+ # @rbs @logger: Logger?
30
+
31
+ # @rbs!
32
+ # attr_accessor enabled: bool
33
+ # attr_accessor auto_fix: bool
34
+ # attr_accessor target_paths: Array[String]
35
+ # attr_accessor skip_paths: Array[String]
36
+ # attr_accessor dry_run: bool
37
+ # attr_accessor backup: bool
38
+ # attr_accessor fix_strategy: Symbol
39
+ # attr_accessor debug: bool
40
+ # attr_writer logger: Logger?
41
+ attr_accessor :enabled, :auto_fix, :target_paths, :skip_paths,
42
+ :dry_run, :backup, :fix_strategy, :debug
43
+ attr_writer :logger
44
+
45
+ #: () -> void
46
+ def initialize
47
+ DEFAULTS.each { |key, value| public_send(:"#{key}=", value.dup) }
48
+ end
49
+
50
+ #: () -> Logger
51
+ def logger
52
+ @logger ||= default_logger
53
+ end
54
+
55
+ private
56
+
57
+ #: () -> Logger
58
+ def default_logger
59
+ if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
60
+ ::Logger.new(Rails.root.join("log", "bullematic.log"))
61
+ else
62
+ ::Logger.new($stdout)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,109 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Bullematic
5
+ class Detection
6
+ # @rbs @type: Symbol
7
+ # @rbs @base_class: untyped
8
+ # @rbs @associations: Array[Symbol]
9
+ # @rbs @call_stack: Array[String]
10
+ # @rbs @source_file: String?
11
+ # @rbs @line_number: Integer?
12
+ # @rbs @method_name: String?
13
+
14
+ # @rbs!
15
+ # attr_reader type: Symbol
16
+ # attr_reader base_class: untyped
17
+ # attr_reader associations: Array[Symbol]
18
+ # attr_reader call_stack: Array[String]
19
+ # attr_reader source_file: String?
20
+ # attr_reader line_number: Integer?
21
+ # attr_reader method_name: String?
22
+ attr_reader :type, :base_class, :associations, :call_stack,
23
+ :source_file, :line_number, :method_name
24
+
25
+ # @rbs type: Symbol
26
+ # @rbs base_class: untyped
27
+ # @rbs associations: untyped
28
+ # @rbs call_stack: Array[String]
29
+ # @rbs return: void
30
+ def initialize(type:, base_class:, associations:, call_stack:)
31
+ @type = type
32
+ @base_class = base_class
33
+ @associations = normalize_associations(associations)
34
+ @call_stack = call_stack
35
+ parse_source_location
36
+ end
37
+
38
+ #: () -> String
39
+ def model_class_name
40
+ base_class.to_s
41
+ end
42
+
43
+ #: () -> bool
44
+ def fixable?
45
+ !source_file.nil? &&
46
+ !source_file.empty? &&
47
+ target_path? &&
48
+ !skip_path?
49
+ end
50
+
51
+ private
52
+
53
+ # @rbs assocs: untyped
54
+ # @rbs return: Array[Symbol]
55
+ def normalize_associations(assocs)
56
+ Array(assocs).map(&:to_sym)
57
+ end
58
+
59
+ #: () -> void
60
+ def parse_source_location
61
+ config = Bullematic.configuration
62
+ return unless config
63
+
64
+ target_paths = config.target_paths
65
+
66
+ @call_stack.each do |frame|
67
+ next unless frame.is_a?(String)
68
+
69
+ match = frame.match(/\A(.+):(\d+)(?::in `(.+)')?/)
70
+ next unless match
71
+
72
+ filepath = match[1]
73
+ line = match[2].to_i
74
+ method = match[3]
75
+
76
+ next unless filepath && target_paths.any? { |path| filepath.include?(path) }
77
+
78
+ @source_file = filepath
79
+ @line_number = line
80
+ @method_name = method
81
+ break
82
+ end
83
+ end
84
+
85
+ #: () -> bool
86
+ def target_path?
87
+ return false if source_file.nil?
88
+
89
+ config = Bullematic.configuration
90
+ return false unless config
91
+
92
+ config.target_paths.any? do |path|
93
+ source_file&.include?(path)
94
+ end
95
+ end
96
+
97
+ #: () -> bool
98
+ def skip_path?
99
+ return false if source_file.nil?
100
+
101
+ config = Bullematic.configuration
102
+ return false unless config
103
+
104
+ config.skip_paths.any? do |path|
105
+ source_file&.include?(path)
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,110 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+
6
+ module Bullematic
7
+ class Fixer
8
+ class << self
9
+ #: () -> Array[Detection]
10
+ def detection_queue
11
+ @detection_queue ||= [] #: Array[Detection]
12
+ end
13
+
14
+ # @rbs detection: Detection
15
+ # @rbs return: void
16
+ def queue(detection)
17
+ detection_queue << detection
18
+ end
19
+
20
+ #: () -> void
21
+ def clear
22
+ @detection_queue = [] #: Array[Detection]
23
+ end
24
+
25
+ #: () -> void
26
+ def apply_fixes
27
+ return if detection_queue.empty?
28
+
29
+ logger = BullematicLogger.new
30
+
31
+ grouped = detection_queue.group_by(&:source_file)
32
+
33
+ grouped.each do |filepath, detections|
34
+ next unless filepath
35
+
36
+ process_file(filepath, detections, logger)
37
+ rescue ParseError, FixError => e
38
+ logger.log_error(filepath, e)
39
+ rescue StandardError => e
40
+ logger.log_error(filepath, e)
41
+ raise if Bullematic.configuration&.debug
42
+ end
43
+
44
+ logger.log_summary
45
+ clear
46
+ end
47
+
48
+ private
49
+
50
+ # @rbs filepath: String
51
+ # @rbs detections: Array[Detection]
52
+ # @rbs logger: BullematicLogger
53
+ # @rbs return: void
54
+ def process_file(filepath, detections, logger)
55
+ return unless File.exist?(filepath)
56
+
57
+ original_source = File.read(filepath)
58
+ parse_result = AST::Parser.parse_file(filepath)
59
+ finder = AST::Finder.new(parse_result)
60
+ rewriter = AST::Rewriter.new(original_source)
61
+
62
+ detections.each do |detection|
63
+ location = find_query_location(finder, detection)
64
+
65
+ if location.nil?
66
+ logger.log_skip(filepath, detection, "could not locate query")
67
+ next
68
+ end
69
+
70
+ rewriter.add_includes(location, detection.associations)
71
+ logger.log_fix(filepath, detection)
72
+ end
73
+
74
+ new_source = rewriter.rewrite
75
+
76
+ return if new_source == original_source
77
+
78
+ if Bullematic.configuration&.dry_run
79
+ logger.log_dry_run(filepath, original_source, new_source)
80
+ else
81
+ backup_file(filepath) if Bullematic.configuration&.backup
82
+ File.write(filepath, new_source)
83
+ end
84
+ end
85
+
86
+ # @rbs finder: AST::Finder
87
+ # @rbs detection: Detection
88
+ # @rbs return: AST::Finder::QueryLocation?
89
+ def find_query_location(finder, detection)
90
+ queries = finder.find_model_queries(
91
+ detection.model_class_name,
92
+ line_number: detection.line_number
93
+ )
94
+
95
+ return queries.first unless queries.empty?
96
+
97
+ return nil unless detection.line_number
98
+
99
+ finder.find_query_at_line(detection.line_number)
100
+ end
101
+
102
+ # @rbs filepath: String
103
+ # @rbs return: void
104
+ def backup_file(filepath)
105
+ backup_path = "#{filepath}.bullematic.bak"
106
+ FileUtils.cp(filepath, backup_path)
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,37 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Bullematic
5
+ module Integrations
6
+ module Minitest
7
+ class << self
8
+ #: () -> void
9
+ def setup
10
+ ::Minitest.after_run do
11
+ Bullematic::Fixer.apply_fixes if Bullematic.configuration&.enabled && Bullematic.configuration&.auto_fix
12
+ end
13
+ end
14
+ end
15
+
16
+ module TestHelper
17
+ #: () -> void
18
+ def setup
19
+ super
20
+ return unless Bullematic.enabled?
21
+
22
+ Bullet.start_request if defined?(Bullet) && Bullet.enable?
23
+ Bullematic::Fixer.clear
24
+ end
25
+
26
+ #: () -> void
27
+ def teardown
28
+ if Bullematic.enabled? && defined?(Bullet) && Bullet.enable?
29
+ Bullematic::Notifier.process_notifications if Bullet.notification?
30
+ Bullet.end_request
31
+ end
32
+ super
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end