Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
640f125
Add parallel CI support for DeprecationTracker
JuanVqz Apr 2, 2026
af5cdb5
Fall back to :save when mode is not provided
JuanVqz Apr 2, 2026
81b1c8b
Remove detect_node_index auto-detection
JuanVqz Apr 2, 2026
400720a
Extract target_path method for write path resolution
JuanVqz Apr 2, 2026
d384360
Simplify normalized_deprecation_messages to always use target_path
JuanVqz Apr 2, 2026
638834f
Rename read_shitlist to read_json
JuanVqz Apr 2, 2026
5fc9594
Rename read_json to read_stored_deprecations
JuanVqz Apr 2, 2026
8963b6d
Rename shitlist variable to stored in compare method
JuanVqz Apr 2, 2026
e9ab6de
Make path argument explicit in read_stored_deprecations
JuanVqz Apr 2, 2026
1066282
Rename create_temp_shitlist to create_temp_file
JuanVqz Apr 2, 2026
c0937e7
Rename new_shitlist variable to temp_file
JuanVqz Apr 2, 2026
100a59f
Rename read_stored_deprecations to read_json
JuanVqz Apr 2, 2026
904b2f7
Fix NoMethodError when mode is nil in constructor
JuanVqz Apr 2, 2026
46aaa6b
Memoize normalized_deprecation_messages to avoid repeated disk reads
JuanVqz Apr 2, 2026
4ba8b41
Fix shell injection in diff method for paths with spaces
JuanVqz Apr 2, 2026
7f4e9e1
Rename DEFAULT_SHITLIST_PATH to DEFAULT_PATH
JuanVqz Apr 2, 2026
261b60d
Use idiomatic any? instead of length > 0
JuanVqz Apr 2, 2026
0d27b0d
Fix Ruby 2.4 syntax error in spaces test
JuanVqz Apr 2, 2026
9cde4c0
Revert Shellwords fix for paths with spaces
JuanVqz Apr 2, 2026
f8c203d
Use File.extname for shard_path extension handling
JuanVqz Apr 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 57 additions & 31 deletions lib/deprecation_tracker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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!

Expand All @@ -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
Expand All @@ -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
113 changes: 113 additions & 0 deletions spec/deprecation_tracker_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 }

Expand Down
Loading