Class: DeprecationTracker

Inherits:
Object
  • Object
show all
Includes:
KernelWarnTracker
Defined in:
lib/deprecation_tracker.rb,
lib/deprecation_tracker/valid_modes.rb,
lib/deprecation_tracker/shard_merger.rb

Defined Under Namespace

Modules: KernelWarnTracker, MinitestExtension Classes: ShardMerger

Constant Summary collapse

UnexpectedDeprecations =
Class.new(StandardError)
VALID_MODES =
%i[save compare].freeze
DEFAULT_PATH =
"spec/support/deprecation_warning.shitlist.json"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from KernelWarnTracker

callbacks, #warn

Constructor Details

#initialize(shitlist_path, transform_message = nil, mode = :save, node_index: nil) ⇒ DeprecationTracker

Returns a new instance of DeprecationTracker.



141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/deprecation_tracker.rb', line 141

def initialize(shitlist_path, transform_message = nil, mode = :save, node_index: nil)
  @shitlist_path = shitlist_path
  @transform_message = transform_message || -> (message) { message }
  @deprecation_messages = {}
  @mode = mode ? mode.to_sym : :save
  unless self.class.valid_mode?(@mode)
    raise ArgumentError, "mode must be one of: #{self.class.valid_modes_display}. Got: #{mode.inspect}"
  end
  if @mode == :compare && node_index
    raise ArgumentError, "node_index cannot be used with compare mode"
  end
  @node_index = node_index
end

Instance Attribute Details

#bucketObject

Returns the value of attribute bucket.



139
140
141
# File 'lib/deprecation_tracker.rb', line 139

def bucket
  @bucket
end

#deprecation_messagesObject (readonly)

Returns the value of attribute deprecation_messages.



139
140
141
# File 'lib/deprecation_tracker.rb', line 139

def deprecation_messages
  @deprecation_messages
end

#modeObject (readonly)

Returns the value of attribute mode.



139
140
141
# File 'lib/deprecation_tracker.rb', line 139

def mode
  @mode
end

#node_indexObject (readonly)

Returns the value of attribute node_index.



139
140
141
# File 'lib/deprecation_tracker.rb', line 139

def node_index
  @node_index
end

#shitlist_pathObject (readonly)

Returns the value of attribute shitlist_path.



139
140
141
# File 'lib/deprecation_tracker.rb', line 139

def shitlist_path
  @shitlist_path
end

#transform_messageObject (readonly)

Returns the value of attribute transform_message.



139
140
141
# File 'lib/deprecation_tracker.rb', line 139

def transform_message
  @transform_message
end

Class Method Details

.init_tracker(opts = {}) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/deprecation_tracker.rb', line 80

def self.init_tracker(opts = {})
  shitlist_path = opts[:shitlist_path] || DEFAULT_PATH
  mode = sanitize_mode(opts[:mode] || ENV["DEPRECATION_TRACKER"]) || :save
  transform_message = opts[:transform_message]
  node_index = opts[:node_index]
  deprecation_tracker = DeprecationTracker.new(shitlist_path, transform_message, mode, node_index: node_index)
  # Since Rails 7.1 the preferred way to track deprecations is to use the deprecation trackers via
  # `Rails.application.deprecators`.
  # We fallback to tracking deprecations via the ActiveSupport singleton object if Rails.application.deprecators is
  # not defined for older Rails versions.
  if defined?(Rails) && defined?(Rails.application) && defined?(Rails.application.deprecators)
    Rails.application.deprecators.each do |deprecator|
      deprecator.behavior << -> (message, _callstack = nil, _deprecation_horizon = nil, _gem_name = nil) {
        deprecation_tracker.add(message)
      }
    end
  elsif defined?(ActiveSupport)
    ActiveSupport::Deprecation.behavior << -> (message, _callstack = nil, _deprecation_horizon = nil, _gem_name = nil) {
      deprecation_tracker.add(message)
    }
  end
  KernelWarnTracker.callbacks << -> (message) { deprecation_tracker.add(message) }

  deprecation_tracker
end

.merge_shards(base_path, delete_shards: false) ⇒ Object



134
135
136
137
# File 'lib/deprecation_tracker.rb', line 134

def self.merge_shards(base_path, delete_shards: false)
  require_relative "deprecation_tracker/shard_merger"
  ShardMerger.new(base_path, delete_shards: delete_shards).merge[:result]
end

.sanitize_mode(mode) ⇒ Object

Returns the mode as-is, or nil when it is blank. A blank DEPRECATION_TRACKER (e.g. DEPRECATION_TRACKER= rspec) is truthy in Ruby, so callers can use this to treat an empty value as unset and fall back to the default mode.



16
17
18
19
20
21
# File 'lib/deprecation_tracker/valid_modes.rb', line 16

def self.sanitize_mode(mode)
  return if mode.nil?

  stripped = mode.to_s.strip
  stripped.empty? ? nil : stripped
end

.track_minitest(opts = {}) ⇒ Object



124
125
126
127
128
129
130
131
132
# File 'lib/deprecation_tracker.rb', line 124

def self.track_minitest(opts = {})
  tracker = init_tracker(opts)

  Minitest.after_run do
    tracker.after_run
  end

  ActiveSupport::TestCase.include(MinitestExtension.new(tracker))
end

.track_rspec(rspec_config, opts = {}) ⇒ Object



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/deprecation_tracker.rb', line 106

def self.track_rspec(rspec_config, opts = {})
  deprecation_tracker = init_tracker(opts)

  rspec_config.around do |example|
    deprecation_tracker.bucket = example..fetch(:rerun_file_path)

    begin
      example.run
    ensure
      deprecation_tracker.bucket = nil
    end
  end

  rspec_config.after(:suite) do
    deprecation_tracker.after_run
  end
end

.valid_mode?(mode) ⇒ Boolean

Returns:

  • (Boolean)


9
10
11
# File 'lib/deprecation_tracker/valid_modes.rb', line 9

def self.valid_mode?(mode)
  mode && VALID_MODES.include?(mode.to_sym)
end

.valid_modes_displayObject

Human-readable list of valid modes for error messages and CLI help text.



5
6
7
# File 'lib/deprecation_tracker/valid_modes.rb', line 5

def self.valid_modes_display
  @valid_modes_display ||= VALID_MODES.map(&:to_s).join(", ")
end

Instance Method Details

#add(message) ⇒ Object



168
169
170
171
172
# File 'lib/deprecation_tracker.rb', line 168

def add(message)
  return if bucket.nil?

  @deprecation_messages[bucket] << transform_message.(message)
end

#after_runObject



179
180
181
182
183
184
185
# File 'lib/deprecation_tracker.rb', line 179

def after_run
  if mode == :save
    save
  elsif mode == :compare
    compare
  end
end

#compareObject



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/deprecation_tracker.rb', line 187

def compare
  stored = read_json(shitlist_path)

  changed_buckets = []

  normalized_deprecation_messages.each do |bucket, messages|
    if stored[bucket] != messages
      changed_buckets << bucket
    end
  end

  if changed_buckets.any?
    message = <<-MESSAGE
      ⚠️  Deprecation warnings have changed!

      Code called by the following spec files is now generating different deprecation warnings:

      #{changed_buckets.join("\n")}

      To check your failures locally, you can run:

      DEPRECATION_TRACKER=compare bundle exec rspec #{changed_buckets.join(" ")}

      Here is a diff between what is expected and what was generated by this process:

      #{diff}

      See \e[4;37mdev-docs/testing/deprecation_tracker.md\e[0;31m for more information.
    MESSAGE

    raise UnexpectedDeprecations, NextRails::Tint(message).red
  end
end

#create_if_path_does_not_exist(path) ⇒ Object



236
237
238
239
240
241
# File 'lib/deprecation_tracker.rb', line 236

def create_if_path_does_not_exist(path)
  dirname = File.dirname(path)
  unless File.directory?(dirname)
    FileUtils.mkdir_p(dirname)
  end
end

#create_temp_fileObject



243
244
245
246
247
248
249
# File 'lib/deprecation_tracker.rb', line 243

def create_temp_file
  temp_file = Tempfile.new("temp-deprecation-tracker-shitlist")
  temp_file.write(JSON.pretty_generate(normalized_deprecation_messages))
  temp_file.flush

  temp_file
end

#diffObject



221
222
223
224
225
226
# File 'lib/deprecation_tracker.rb', line 221

def diff
  temp_file = create_temp_file
  `git diff --no-index #{shitlist_path} #{temp_file.path}`
ensure
  temp_file.delete
end

#normalized_deprecation_messagesObject

Normalize deprecation messages to reduce noise from file output and test files to be tracked with separate test runs



252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/deprecation_tracker.rb', line 252

def normalized_deprecation_messages
  @normalized_deprecation_messages ||= begin
    normalized = read_json(target_path).merge(deprecation_messages).each_with_object({}) do |(bucket, messages), hash|
      hash[bucket] = messages.sort
    end

    # not using `to_h` here to support older ruby versions
    {}.tap do |h|
      normalized.reject {|_key, value| value.empty? }.sort_by {|key, _value| key }.each do |k ,v|
        h[k] = v
      end
    end
  end
end

#parallel?Boolean

Returns:

  • (Boolean)


155
156
157
# File 'lib/deprecation_tracker.rb', line 155

def parallel?
  !@node_index.nil?
end

#read_json(path) ⇒ Object



267
268
269
270
271
272
# File 'lib/deprecation_tracker.rb', line 267

def read_json(path)
  return {} unless File.exist?(path)
  JSON.parse(File.read(path))
rescue JSON::ParserError => e
  raise "#{path} is not valid JSON: #{e.message}"
end

#saveObject



228
229
230
231
232
233
234
# File 'lib/deprecation_tracker.rb', line 228

def save
  temp_file = create_temp_file
  create_if_path_does_not_exist(target_path)
  FileUtils.cp(temp_file.path, target_path)
ensure
  temp_file.delete if temp_file
end

#shard_pathObject



159
160
161
162
# File 'lib/deprecation_tracker.rb', line 159

def shard_path
  ext = File.extname(shitlist_path)
  "#{shitlist_path.chomp(ext)}.node-#{node_index}#{ext}"
end

#target_pathObject



164
165
166
# File 'lib/deprecation_tracker.rb', line 164

def target_path
  parallel? ? shard_path : shitlist_path
end