diff --git a/lib/deprecation_tracker.rb b/lib/deprecation_tracker.rb index 47700c8..8f03a3e 100644 --- a/lib/deprecation_tracker.rb +++ b/lib/deprecation_tracker.rb @@ -73,11 +73,14 @@ def Kernel.warn(*args, &block) end end + DEFAULT_PATH = "spec/support/deprecation_warning.shitlist.json" + def self.init_tracker(opts = {}) - shitlist_path = opts[:shitlist_path] - mode = opts[:mode] + shitlist_path = opts[:shitlist_path] || DEFAULT_PATH + mode = opts[:mode] || ENV["DEPRECATION_TRACKER"] || :save transform_message = opts[:transform_message] - deprecation_tracker = DeprecationTracker.new(shitlist_path, transform_message, mode) + 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 @@ -126,13 +129,27 @@ def self.track_minitest(opts = {}) ActiveSupport::TestCase.include(MinitestExtension.new(tracker)) end - attr_reader :deprecation_messages, :shitlist_path, :transform_message, :bucket, :mode + attr_reader :deprecation_messages, :shitlist_path, :transform_message, :bucket, :mode, :node_index - def initialize(shitlist_path, transform_message = nil, mode = :save) + 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.to_sym + @mode = mode ? mode.to_sym : :save + @node_index = node_index + end + + def parallel? + !@node_index.nil? + end + + def shard_path + ext = File.extname(shitlist_path) + "#{shitlist_path.chomp(ext)}.node-#{node_index}#{ext}" + end + + def target_path + parallel? ? shard_path : shitlist_path end def add(message) @@ -155,16 +172,23 @@ def after_run end def compare - shitlist = read_shitlist + stored = read_json(shitlist_path) changed_buckets = [] - normalized_deprecation_messages.each do |bucket, messages| - if shitlist[bucket] != messages + buckets_to_check = if parallel? + # In parallel mode, only check buckets that this node actually ran + normalized_deprecation_messages.select { |bucket, _| deprecation_messages.key?(bucket) } + else + normalized_deprecation_messages + end + + buckets_to_check.each do |bucket, messages| + if stored[bucket] != messages changed_buckets << bucket end end - if changed_buckets.length > 0 + if changed_buckets.any? message = <<-MESSAGE ⚠️ Deprecation warnings have changed! @@ -188,28 +212,28 @@ def compare end def diff - new_shitlist = create_temp_shitlist - `git diff --no-index #{shitlist_path} #{new_shitlist.path}` + temp_file = create_temp_file + `git diff --no-index #{shitlist_path} #{temp_file.path}` ensure - new_shitlist.delete + temp_file.delete end def save - new_shitlist = create_temp_shitlist - create_if_shitlist_path_does_not_exist - FileUtils.cp(new_shitlist.path, shitlist_path) + temp_file = create_temp_file + create_if_path_does_not_exist(target_path) + FileUtils.cp(temp_file.path, target_path) ensure - new_shitlist.delete if new_shitlist + temp_file.delete if temp_file end - def create_if_shitlist_path_does_not_exist - dirname = File.dirname(shitlist_path) + def create_if_path_does_not_exist(path) + dirname = File.dirname(path) unless File.directory?(dirname) FileUtils.mkdir_p(dirname) end end - def create_temp_shitlist + def create_temp_file temp_file = Tempfile.new("temp-deprecation-tracker-shitlist") temp_file.write(JSON.pretty_generate(normalized_deprecation_messages)) temp_file.flush @@ -219,22 +243,24 @@ def create_temp_shitlist # Normalize deprecation messages to reduce noise from file output and test files to be tracked with separate test runs def normalized_deprecation_messages - normalized = read_shitlist.merge(deprecation_messages).each_with_object({}) do |(bucket, messages), hash| - hash[bucket] = messages.sort - end + @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 + # 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 - def read_shitlist - return {} unless File.exist?(shitlist_path) - JSON.parse(File.read(shitlist_path)) + def read_json(path) + return {} unless File.exist?(path) + JSON.parse(File.read(path)) rescue JSON::ParserError => e - raise "#{shitlist_path} is not valid JSON: #{e.message}" + raise "#{path} is not valid JSON: #{e.message}" end end diff --git a/spec/deprecation_tracker_spec.rb b/spec/deprecation_tracker_spec.rb index 330723e..24fd311 100644 --- a/spec/deprecation_tracker_spec.rb +++ b/spec/deprecation_tracker_spec.rb @@ -243,6 +243,11 @@ tracker.after_run end + it "defaults to save mode when mode is nil" do + tracker = DeprecationTracker.new(shitlist_path, nil, nil) + expect(tracker.mode).to eq(:save) + end + it "does not save nor compare if mode is invalid" do tracker = DeprecationTracker.new(shitlist_path, nil, "random_stuff") expect(tracker).not_to receive(:save) @@ -293,6 +298,114 @@ def self.behavior end end + describe "#parallel?" do + it "returns false when node_index is nil" do + tracker = DeprecationTracker.new(shitlist_path) + expect(tracker.parallel?).to be false + end + + it "returns true when node_index is set" do + tracker = DeprecationTracker.new(shitlist_path, nil, :save, node_index: "0") + expect(tracker.parallel?).to be true + end + end + + describe "#shard_path" do + it "derives shard path from shitlist_path and node_index" do + tracker = DeprecationTracker.new("spec/support/deprecation_warning.shitlist.json", nil, :save, node_index: "2") + expect(tracker.shard_path).to eq("spec/support/deprecation_warning.shitlist.node-2.json") + end + + it "works with custom shitlist paths" do + tracker = DeprecationTracker.new("tmp/my_custom_shitlist.json", nil, :save, node_index: "0") + expect(tracker.shard_path).to eq("tmp/my_custom_shitlist.node-0.json") + end + end + + describe "parallel mode" do + describe "#save" do + it "writes to shard path when node_index is set" do + tracker = DeprecationTracker.new(shitlist_path, nil, :save, node_index: "0") + tracker.bucket = "bucket 1" + tracker.add("a") + tracker.save + + ext = File.extname(shitlist_path) + expected_shard = "#{shitlist_path.chomp(ext)}.node-0#{ext}" + expect(File.exist?(expected_shard)).to be true + expect(JSON.parse(File.read(expected_shard))).to eq("bucket 1" => ["a"]) + expect(File.exist?(shitlist_path)).to be false + + FileUtils.rm(expected_shard) + end + + it "merges with existing shard data on subsequent saves" do + ext = File.extname(shitlist_path) + shard = "#{shitlist_path.chomp(ext)}.node-0#{ext}" + + tracker1 = DeprecationTracker.new(shitlist_path, nil, :save, node_index: "0") + tracker1.bucket = "bucket 1" + tracker1.add("a") + tracker1.save + + tracker2 = DeprecationTracker.new(shitlist_path, nil, :save, node_index: "0") + tracker2.bucket = "bucket 2" + tracker2.add("b") + tracker2.save + + data = JSON.parse(File.read(shard)) + expect(data).to eq("bucket 1" => ["a"], "bucket 2" => ["b"]) + + FileUtils.rm(shard) + end + end + + describe "#compare" do + it "only checks buckets that this node ran" do + # Set up canonical shitlist with two buckets + setup_tracker = DeprecationTracker.new(shitlist_path) + setup_tracker.bucket = "bucket 1" + setup_tracker.add("a") + setup_tracker.bucket = "bucket 2" + setup_tracker.add("b") + setup_tracker.save + + # Parallel node only runs bucket 2 with matching deprecations + tracker = DeprecationTracker.new(shitlist_path, nil, :compare, node_index: "0") + tracker.bucket = "bucket 2" + tracker.add("b") + + expect { tracker.compare }.not_to raise_error + end + + it "raises when this node's buckets have changed" do + setup_tracker = DeprecationTracker.new(shitlist_path) + setup_tracker.bucket = "bucket 1" + setup_tracker.add("a") + setup_tracker.save + + tracker = DeprecationTracker.new(shitlist_path, nil, :compare, node_index: "0") + tracker.bucket = "bucket 1" + tracker.add("different") + + expect { tracker.compare }.to raise_error(DeprecationTracker::UnexpectedDeprecations) + end + end + end + + describe "default values" do + it "uses default shitlist_path when not provided" do + tracker = DeprecationTracker.init_tracker(mode: "save") + expect(tracker.shitlist_path).to eq("spec/support/deprecation_warning.shitlist.json") + end + + it "uses ENV['DEPRECATION_TRACKER'] as default mode" do + stub_const("ENV", ENV.to_h.merge("DEPRECATION_TRACKER" => "compare")) + tracker = DeprecationTracker.init_tracker({}) + expect(tracker.mode).to eq(:compare) + end + end + describe DeprecationTracker::KernelWarnTracker do before { DeprecationTracker::KernelWarnTracker.callbacks.clear }