Class: DeprecationTracker

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

Overview

A shitlist for deprecation warnings during test runs. It has two modes: “save” and “compare”

DEPRECATION_TRACKER=save Record deprecation warnings, grouped by spec file. After the test run, save to a file.

DEPRECATION_TRACKER=compare Tracks deprecation warnings, grouped by spec file. After the test run, compare against shitlist of expected deprecation warnings. If anything is added or removed, raise an error with a diff of the changes.

Defined Under Namespace

Modules: KernelWarnTracker, MinitestExtension Classes: ShardMerger

Constant Summary collapse

UnexpectedDeprecations =
Class.new(StandardError)
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.



139
140
141
142
143
144
145
146
147
148
# File 'lib/deprecation_tracker.rb', line 139

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
  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.



137
138
139
# File 'lib/deprecation_tracker.rb', line 137

def bucket
  @bucket
end

#deprecation_messagesObject (readonly)

Returns the value of attribute deprecation_messages.



137
138
139
# File 'lib/deprecation_tracker.rb', line 137

def deprecation_messages
  @deprecation_messages
end

#modeObject (readonly)

Returns the value of attribute mode.



137
138
139
# File 'lib/deprecation_tracker.rb', line 137

def mode
  @mode
end

#node_indexObject (readonly)

Returns the value of attribute node_index.



137
138
139
# File 'lib/deprecation_tracker.rb', line 137

def node_index
  @node_index
end

#shitlist_pathObject (readonly)

Returns the value of attribute shitlist_path.



137
138
139
# File 'lib/deprecation_tracker.rb', line 137

def shitlist_path
  @shitlist_path
end

#transform_messageObject (readonly)

Returns the value of attribute transform_message.



137
138
139
# File 'lib/deprecation_tracker.rb', line 137

def transform_message
  @transform_message
end

Class Method Details

.init_tracker(opts = {}) ⇒ Object



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

def self.init_tracker(opts = {})
  shitlist_path = opts[:shitlist_path] || DEFAULT_PATH
  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



132
133
134
135
# File 'lib/deprecation_tracker.rb', line 132

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

.track_minitest(opts = {}) ⇒ Object



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

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



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

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

Instance Method Details

#add(message) ⇒ Object



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

def add(message)
  return if bucket.nil?

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

#after_runObject



174
175
176
177
178
179
180
# File 'lib/deprecation_tracker.rb', line 174

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

#compareObject



182
183
184
185
186
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
# File 'lib/deprecation_tracker.rb', line 182

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



231
232
233
234
235
236
# File 'lib/deprecation_tracker.rb', line 231

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



238
239
240
241
242
243
244
# File 'lib/deprecation_tracker.rb', line 238

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



216
217
218
219
220
221
# File 'lib/deprecation_tracker.rb', line 216

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



247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/deprecation_tracker.rb', line 247

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)


150
151
152
# File 'lib/deprecation_tracker.rb', line 150

def parallel?
  !@node_index.nil?
end

#read_json(path) ⇒ Object



262
263
264
265
266
267
# File 'lib/deprecation_tracker.rb', line 262

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



223
224
225
226
227
228
229
# File 'lib/deprecation_tracker.rb', line 223

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



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

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

#target_pathObject



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

def target_path
  parallel? ? shard_path : shitlist_path
end