├── .rspec ├── lib ├── ten_years_rails │ ├── version.rb │ ├── gem_info.rb │ └── bundle_report.rb ├── ten_years_rails.rb └── deprecation_tracker.rb ├── overlord.yml ├── .travis.yml ├── Rakefile ├── dev.yml ├── bin ├── setup └── console ├── spec ├── ten_years_rails_spec.rb ├── spec_helper.rb ├── ten_years_rails │ └── gem_info_spec.rb └── deprecation_tracker_spec.rb ├── exe ├── next ├── next.sh ├── gem-next-diff ├── bundle_report └── deprecations ├── Gemfile ├── .gitignore ├── .github └── dependabot.yml ├── LICENSE.txt ├── ten_years_rails.gemspec ├── Gemfile.lock ├── README.md └── deprecation_tracker.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /lib/ten_years_rails/version.rb: -------------------------------------------------------------------------------- 1 | module TenYearsRails 2 | VERSION = "1.0.2" 3 | end 4 | -------------------------------------------------------------------------------- /overlord.yml: -------------------------------------------------------------------------------- 1 | tier: 4 2 | team: backend-infrastructure 3 | classification: library 4 | slack_name: "#help-back-end" 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.2.10 5 | - 2.3.3 6 | before_install: gem install bundler -v 1.16.1 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /dev.yml: -------------------------------------------------------------------------------- 1 | version: 1.1 2 | 3 | setup: 4 | ruby: 2.6.6 5 | bundler: 1.17.2 6 | 7 | commands: 8 | test: 9 | - "bundle exec rspec spec" 10 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /spec/ten_years_rails_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe TenYearsRails do 2 | it "has a version number" do 3 | expect(TenYearsRails::VERSION).not_to be nil 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /exe/next: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Ruby wrapper around next.sh so it works with Rubygems 3 | exe_dir = File.expand_path(File.dirname(__FILE__)) 4 | exec(File.join(exe_dir, 'next.sh'), *ARGV) 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in ten_years_rails.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | .byebug_history 11 | 12 | # rspec failure tracking 13 | .rspec_status 14 | 15 | .ruby-version 16 | -------------------------------------------------------------------------------- /lib/ten_years_rails.rb: -------------------------------------------------------------------------------- 1 | require "ten_years_rails/gem_info" 2 | require "ten_years_rails/version" 3 | require "ten_years_rails/bundle_report" 4 | require "deprecation_tracker" 5 | 6 | module TenYearsRails 7 | # Your code goes here... 8 | end 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | registries: 4 | rubygems-github: 5 | type: rubygems-server 6 | url: https://rubygems.pkg.github.com/clio 7 | token: "${{ secrets.DEPENDABOT_GITHUB_TOKEN }}" 8 | updates: 9 | - package-ecosystem: bundler 10 | directory: "/" 11 | schedule: 12 | interval: monthly 13 | open-pull-requests-limit: 0 14 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "ten_years_rails" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "ten_years_rails" 3 | 4 | RSpec.configure do |config| 5 | # Enable flags like --only-failures and --next-failure 6 | config.example_status_persistence_file_path = ".rspec_status" 7 | 8 | # Disable RSpec exposing methods globally on `Module` and `main` 9 | config.disable_monkey_patching! 10 | 11 | config.expect_with :rspec do |c| 12 | c.syntax = :expect 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/ten_years_rails/gem_info_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "timecop" 3 | 4 | RSpec.describe TenYearsRails::GemInfo do 5 | let(:release_date) { Time.utc(2019, 7, 6, 0, 0, 0) } 6 | let(:now) { Time.utc(2019, 7, 6, 12, 0, 0) } 7 | let(:spec) do 8 | Gem::Specification.new do |s| 9 | s.date = release_date 10 | end 11 | end 12 | 13 | subject { TenYearsRails::GemInfo.new(spec) } 14 | 15 | describe "#age" do 16 | around do |example| 17 | Timecop.travel(now) do 18 | example.run 19 | end 20 | end 21 | 22 | context "when ActionView is available" do 23 | it "returns a time ago" do 24 | expect(subject.age).to eq("about 12 hours ago") 25 | end 26 | end 27 | 28 | context "when ActionView is not available" do 29 | let(:result) { now.strftime("%b %e, %Y") } 30 | 31 | before do 32 | subject.instance_eval('undef :time_ago_in_words') 33 | end 34 | 35 | it "returns a date" do 36 | expect(subject.age).to eq(result) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Jordan Raine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /ten_years_rails.gemspec: -------------------------------------------------------------------------------- 1 | 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "ten_years_rails/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "ten_years_rails" 8 | spec.version = TenYearsRails::VERSION 9 | spec.authors = ["Jordan Raine", "Ernesto Tagwerker"] 10 | spec.email = ["jnraine@gmail.com", "ernesto@ombulabs.com"] 11 | 12 | spec.summary = %q{Companion code to Ten Years of Rails Upgrades} 13 | spec.description = %q{A set of handy tools to upgrade your Rails application and keep it up to date} 14 | spec.homepage = "https://github.com/clio/ten_years_rails" 15 | spec.license = "MIT" 16 | 17 | spec.required_ruby_version = ">= 2.3.0" 18 | 19 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 20 | f.match(%r{^(test|spec|features)/}) 21 | end 22 | spec.bindir = "exe" 23 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 24 | spec.require_paths = ["lib"] 25 | 26 | spec.add_dependency "rainbow", "~> 3.0.0" 27 | spec.add_development_dependency "bundler" 28 | spec.add_development_dependency "rake", "~> 13.0" 29 | spec.add_development_dependency "rspec", "~> 3.0" 30 | spec.add_development_dependency "timecop", "~> 0.9.1" 31 | spec.add_runtime_dependency "actionview", ">= 5.2.3", "< 6.1.0" 32 | end 33 | -------------------------------------------------------------------------------- /exe/next.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [[ "${@}" == "--init" ]]; then 3 | # Add next? top of Gemfile 4 | cat <<-STRING > Gemfile.tmp 5 | def next? 6 | File.basename(__FILE__) == "Gemfile.next" 7 | end 8 | STRING 9 | cat Gemfile >> Gemfile.tmp 10 | mv Gemfile.tmp Gemfile 11 | 12 | ln -s Gemfile Gemfile.next 13 | echo <<-MESSAGE 14 | Created Gemfile.next (a symlink to your Gemfile). Your Gemfile has been modified to support dual-booting! 15 | 16 | There's just one more step: modify your Gemfile to use a newer version of Rails using the \`next?\` helper method. 17 | 18 | For example, here's how to go from 5.2.3 to 6.0: 19 | 20 | if next? 21 | gem "rails", "6.0.0" 22 | else 23 | gem "rails", "5.2.3" 24 | end 25 | MESSAGE 26 | exit $? 27 | fi 28 | 29 | if [[ "${@}" =~ ^bundle ]]; then 30 | BUNDLE_GEMFILE=Gemfile.next BUNDLE_CACHE_PATH=vendor/cache.next $@ 31 | else 32 | BUNDLE_GEMFILE=Gemfile.next BUNDLE_CACHE_PATH=vendor/cache.next bundle exec $@ 33 | fi 34 | 35 | COMMAND_EXIT=$? 36 | 37 | GEM_NOT_FOUND=7 # https://github.com/bundler/bundler/blob/master/lib/bundler/errors.rb#L35 38 | EXECUTABLE_NOT_FOUND=127 # https://github.com/bundler/bundler/blob/master/lib/bundler/cli/exec.rb#L62 39 | if [[ $COMMAND_EXIT -eq $GEM_NOT_FOUND || $COMMAND_EXIT -eq $EXECUTABLE_NOT_FOUND ]]; then 40 | BLUE='\033[0;34m' 41 | UNDERLINE_WHITE='\033[37m' 42 | NO_COLOR='\033[0m' 43 | 44 | echo -e "${BLUE}Having trouble running commands with ${UNDERLINE_WHITE}bin/next${BLUE}?" 45 | echo -e "Try running ${UNDERLINE_WHITE}bin/next bundle install${BLUE}, then try your command again.${NO_COLOR}" 46 | fi 47 | 48 | exit $COMMAND_EXIT 49 | -------------------------------------------------------------------------------- /exe/gem-next-diff: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Output a markdown table containing the version differences between the Gemfile and Gemfile.next. 4 | # This can be used as a TODO list to bring the two Gemfiles as close to parity as possible, 5 | # either by upgrading gems within `Gemfile` or downgrading gems within `Gemfile.next`. 6 | # 7 | 8 | def parse_gem_versions(&block) 9 | bundler_output = block.call 10 | bundler_output.split("\n").each_with_object({}) do |line, hash| 11 | next unless line.start_with?("Using") 12 | 13 | gem_name, version = line.split(/\s+/)[1..2] 14 | hash[gem_name] = version 15 | end 16 | end 17 | 18 | rails_gems = [ 19 | "rails", 20 | "activemodel", 21 | "activerecord", 22 | "actionmailer", 23 | "actioncable", 24 | "actionpack", 25 | "actionview", 26 | "activejob", 27 | "activestorage", 28 | "activesupport", 29 | "railties", 30 | ] 31 | 32 | gems = parse_gem_versions { `BUNDLE_CACHE_PATH=vendor/cache BUNDLE_GEMFILE=Gemfile bundle install` } 33 | gems_next = parse_gem_versions { `BUNDLE_CACHE_PATH=vendor/cache.next BUNDLE_GEMFILE=Gemfile.next bundle install` } 34 | all_gem_names = (gems.keys + gems_next.keys).uniq.sort 35 | 36 | puts "| Gem | `Gemfile` version | `Gemfile.next` version | Rails internals |" 37 | puts "|-----|-------------------|------------------------|-----------------| " 38 | all_gem_names.each do |gem_name| 39 | gem_version = gems[gem_name] || "-" 40 | gem_next_version = gems_next[gem_name] || "-" 41 | rails_internal = rails_gems.include?(gem_name) ? "✅" : "" 42 | 43 | if gem_version != gem_next_version 44 | puts "| **#{gem_name}** | #{gem_version} | #{gem_next_version} | #{rails_internal} |" 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /exe/bundle_report: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Print a report on our Gemfile 4 | # Why not just use `bundle outdated`? It doesn't give us the information we care about (and it fails). 5 | # 6 | at_exit do 7 | require "optparse" 8 | require "ten_years_rails" 9 | 10 | options = {} 11 | option_parser = OptionParser.new do |opts| 12 | opts.banner = <<-EOS 13 | Usage: #{$0} [report-type] [options] 14 | 15 | report-type There are two report types available: `outdated` and `compatibility` 16 | 17 | Examples: 18 | #{$0} compatibility --rails-version 5.0 19 | #{$0} outdated 20 | EOS 21 | 22 | opts.separator "" 23 | opts.separator "Options:" 24 | 25 | opts.on("--rails-version [STRING]", "Rails version to check compatibility against (defaults to 5.0)") do |rails_version| 26 | options[:rails_version] = rails_version 27 | end 28 | 29 | opts.on("--include-rails-gems", "Include Rails gems in compatibility report (defaults to false)") do 30 | options[:include_rails_gems] = true 31 | end 32 | 33 | opts.on_tail("-h", "--help", "Show this message") do 34 | puts opts 35 | exit 36 | end 37 | end 38 | 39 | begin 40 | option_parser.parse! 41 | rescue OptionParser::ParseError => e 42 | STDERR.puts Rainbow(e.message).red 43 | puts option_parser 44 | exit 1 45 | end 46 | 47 | report_type = ARGV.first 48 | 49 | case report_type 50 | when "outdated" then TenYearsRails::BundleReport.outdated 51 | else 52 | TenYearsRails::BundleReport.compatibility(rails_version: options.fetch(:rails_version, "5.0"), include_rails_gems: options.fetch(:include_rails_gems, false)) 53 | end 54 | end 55 | 56 | # Needs to happen first 57 | require "bundler/setup" 58 | 59 | require "action_view" 60 | require "active_support/core_ext/object/acts_like" 61 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | ten_years_rails (1.0.2) 5 | actionview (>= 5.2.3, < 6.1.0) 6 | rainbow (~> 3.0.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actionview (6.0.4.1) 12 | activesupport (= 6.0.4.1) 13 | builder (~> 3.1) 14 | erubi (~> 1.4) 15 | rails-dom-testing (~> 2.0) 16 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 17 | activesupport (6.0.4.1) 18 | concurrent-ruby (~> 1.0, >= 1.0.2) 19 | i18n (>= 0.7, < 2) 20 | minitest (~> 5.1) 21 | tzinfo (~> 1.1) 22 | zeitwerk (~> 2.2, >= 2.2.2) 23 | builder (3.2.4) 24 | concurrent-ruby (1.1.9) 25 | crass (1.0.6) 26 | diff-lcs (1.3) 27 | erubi (1.10.0) 28 | i18n (1.8.10) 29 | concurrent-ruby (~> 1.0) 30 | loofah (2.12.0) 31 | crass (~> 1.0.2) 32 | nokogiri (>= 1.5.9) 33 | mini_portile2 (2.6.1) 34 | minitest (5.14.4) 35 | nokogiri (1.12.5) 36 | mini_portile2 (~> 2.6.1) 37 | racc (~> 1.4) 38 | racc (1.5.2) 39 | rails-dom-testing (2.0.3) 40 | activesupport (>= 4.2.0) 41 | nokogiri (>= 1.6) 42 | rails-html-sanitizer (1.4.2) 43 | loofah (~> 2.3) 44 | rainbow (3.0.0) 45 | rake (13.0.1) 46 | rspec (3.8.0) 47 | rspec-core (~> 3.8.0) 48 | rspec-expectations (~> 3.8.0) 49 | rspec-mocks (~> 3.8.0) 50 | rspec-core (3.8.2) 51 | rspec-support (~> 3.8.0) 52 | rspec-expectations (3.8.4) 53 | diff-lcs (>= 1.2.0, < 2.0) 54 | rspec-support (~> 3.8.0) 55 | rspec-mocks (3.8.1) 56 | diff-lcs (>= 1.2.0, < 2.0) 57 | rspec-support (~> 3.8.0) 58 | rspec-support (3.8.2) 59 | thread_safe (0.3.6) 60 | timecop (0.9.1) 61 | tzinfo (1.2.9) 62 | thread_safe (~> 0.1) 63 | zeitwerk (2.4.2) 64 | 65 | PLATFORMS 66 | ruby 67 | 68 | DEPENDENCIES 69 | bundler 70 | rake (~> 13.0) 71 | rspec (~> 3.0) 72 | ten_years_rails! 73 | timecop (~> 0.9.1) 74 | 75 | BUNDLED WITH 76 | 2.2.28 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 🚩 *Clio is not actively fixing any CVEs in this repository. Please use accordingly.* 🚩 2 | 3 | # Ten Years of Rails Upgrades 4 | 5 | This is a companion to the "Ten Years of Rails Upgrades" conference talk. You'll find various utilities that we use at Clio to help us prepare for and complete Rails upgrades. 6 | 7 | These scripts are still early days and may not work in every environment or app. 8 | 9 | I wouldn't recommend adding this to your Gemfile long-term. Rather, try out the scripts and use them as a point of reference. Feel free to tweak them to better fit your environment. 10 | 11 | ## Usage 12 | 13 | ### `bundle_report` 14 | 15 | Learn about your Gemfile and see what needs updating. 16 | 17 | ```bash 18 | # Show all out-of-date gems 19 | bundle_report outdated 20 | # Show five oldest, out-of-date gems 21 | bundle_report outdated | head -n 5 22 | # Show gems that don't work with Rails 5.2.0 23 | bundle_report compatibility --rails-version=5.2.0 24 | bundle_report --help 25 | ``` 26 | 27 | ### Deprecation tracking 28 | 29 | If you're using RSpec, add this snippet to `rails_helper.rb` or `spec_helper.rb` (whichever loads Rails). 30 | 31 | ```ruby 32 | RSpec.configure do |config| 33 | # Tracker deprecation messages in each file 34 | if ENV["DEPRECATION_TRACKER"] 35 | DeprecationTracker.track_rspec( 36 | config, 37 | shitlist_path: "spec/support/deprecation_warning.shitlist.json", 38 | mode: ENV["DEPRECATION_TRACKER"], 39 | transform_message: -> (message) { message.gsub("#{Rails.root}/", "") } 40 | ) 41 | end 42 | end 43 | ``` 44 | 45 | We don't use MiniTest, so there isn't a prebuilt config for it but I suspect it's pretty similar to `DeprecationTracker.track_rspec`. 46 | 47 | Once you have that, you can start using deprecation tracking in your tests: 48 | 49 | ```bash 50 | # Run your tests and save the deprecations to the shitlist 51 | DEPRECATION_TRACKER=save rspec 52 | # Run your tests and raise an error when the deprecations change 53 | DEPRECATION_TRACKER=compare rspec 54 | ``` 55 | 56 | #### `deprecations` command 57 | 58 | Once you have stored your deprecations, you can use `deprecations` to display common warnings, run specs, or update the shitlist file. 59 | 60 | ```bash 61 | deprecations info 62 | deprecations info --pattern "ActiveRecord::Base" 63 | deprecations run 64 | deprecations --help # For more options and examples 65 | ``` 66 | 67 | Right now, the path to the shitlist is hardcoded so make sure you store yours at `spec/support/deprecations.shitlist.json`. 68 | 69 | ### Dual-boot Rails next 70 | 71 | This command helps you dual-boot your application. 72 | 73 | ```bash 74 | next --init # Create Gemfile.next 75 | vim Gemfile # Tweak your dependencies conditionally using `next?` 76 | next bundle install # Install new gems 77 | next rails s # Start server using Gemfile.next 78 | ``` 79 | 80 | ## Installation 81 | 82 | Add this line to your application's Gemfile: 83 | 84 | ```ruby 85 | gem 'ten_years_rails' 86 | ``` 87 | 88 | And then execute: 89 | 90 | $ bundle 91 | 92 | Or install it yourself as: 93 | 94 | $ gem install ten_years_rails 95 | 96 | ## License 97 | 98 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 99 | -------------------------------------------------------------------------------- /lib/ten_years_rails/gem_info.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require "action_view" 3 | rescue LoadError 4 | puts "ActionView not available" 5 | end 6 | 7 | module TenYearsRails 8 | class GemInfo 9 | if defined?(ActionView) 10 | include ActionView::Helpers::DateHelper 11 | end 12 | 13 | class NullGemInfo < GemInfo 14 | def initialize; end 15 | 16 | def age 17 | "-" 18 | end 19 | 20 | def created_at 21 | Time.now 22 | end 23 | 24 | def up_to_date? 25 | false 26 | end 27 | 28 | def version 29 | "NOT FOUND" 30 | end 31 | 32 | def unsatisfied_rails_dependencies(*) 33 | ["unknown"] 34 | end 35 | 36 | def state(_) 37 | :null 38 | end 39 | end 40 | 41 | def self.all 42 | Gem::Specification.each.map do |gem_specification| 43 | new(gem_specification) 44 | end 45 | end 46 | 47 | attr_reader :gem_specification, :version, :name 48 | def initialize(gem_specification) 49 | @gem_specification = gem_specification 50 | @version = gem_specification.version 51 | @name = gem_specification.name 52 | end 53 | 54 | def age 55 | if respond_to?(:time_ago_in_words) 56 | "#{time_ago_in_words(created_at)} ago" 57 | else 58 | created_at.strftime("%b %e, %Y") 59 | end 60 | end 61 | 62 | def sourced_from_git? 63 | !!gem_specification.git_version 64 | end 65 | 66 | def created_at 67 | @created_at ||= gem_specification.date 68 | end 69 | 70 | def up_to_date? 71 | version == latest_version.version 72 | end 73 | 74 | def state(rails_version) 75 | if compatible_with_rails?(rails_version: rails_version) 76 | :compatible 77 | elsif latest_version.compatible_with_rails?(rails_version: rails_version) 78 | :latest_compatible 79 | elsif latest_version.version == "NOT FOUND" 80 | :no_new_version 81 | else 82 | :incompatible 83 | end 84 | end 85 | 86 | def latest_version 87 | @latest_version ||= begin 88 | latest_gem_specification = Gem.latest_spec_for(name) 89 | if latest_gem_specification 90 | GemInfo.new(latest_gem_specification) 91 | else 92 | NullGemInfo.new 93 | end 94 | end 95 | end 96 | 97 | def compatible_with_rails?(rails_version: Gem::Version.new("5.0")) 98 | unsatisfied_rails_dependencies(rails_version: rails_version).empty? 99 | end 100 | 101 | def unsatisfied_rails_dependencies(rails_version:) 102 | rails_dependencies = gem_specification.runtime_dependencies.select {|dependency| rails_gems.include?(dependency.name) } 103 | 104 | rails_dependencies.reject do |rails_dependency| 105 | rails_dependency.requirement.satisfied_by?(Gem::Version.new(rails_version)) 106 | end 107 | end 108 | 109 | def from_rails? 110 | rails_gems.include?(name) 111 | end 112 | 113 | private def rails_gems 114 | [ 115 | "rails", 116 | "activemodel", 117 | "activerecord", 118 | "actionmailer", 119 | "actioncable", 120 | "actionpack", 121 | "actionview", 122 | "activejob", 123 | "activestorage", 124 | "activesupport", 125 | "railties", 126 | ] 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /lib/ten_years_rails/bundle_report.rb: -------------------------------------------------------------------------------- 1 | require "rainbow" 2 | require "cgi" 3 | require "erb" 4 | require "json" 5 | 6 | module TenYearsRails 7 | class BundleReport 8 | def self.compatibility(rails_version:, include_rails_gems:) 9 | incompatible_gems = TenYearsRails::GemInfo.all.reject do |gem| 10 | gem.compatible_with_rails?(rails_version: rails_version) || (!include_rails_gems && gem.from_rails?) 11 | end.sort_by do |gem| 12 | [ 13 | gem.latest_version.compatible_with_rails?(rails_version: rails_version) ? 0 : 1, 14 | gem.name 15 | ].join("-") 16 | end 17 | 18 | incompatible_gems_by_state = incompatible_gems.group_by { |gem| gem.state(rails_version) } 19 | 20 | template = <<~ERB 21 | <% if incompatible_gems_by_state[:latest_compatible] -%> 22 | <%= Rainbow("=> Incompatible with Rails #{rails_version} (with new versions that are compatible):").white.bold %> 23 | <%= Rainbow("These gems will need to be upgraded before upgrading to Rails #{rails_version}.").italic %> 24 | 25 | <% incompatible_gems_by_state[:latest_compatible].each do |gem| -%> 26 | <%= gem_header(gem) %> - upgrade to <%= gem.latest_version.version %> 27 | <% end -%> 28 | 29 | <% end -%> 30 | <% if incompatible_gems_by_state[:incompatible] -%> 31 | <%= Rainbow("=> Incompatible with Rails #{rails_version} (with no new compatible versions):").white.bold %> 32 | <%= Rainbow("These gems will need to be removed or replaced before upgrading to Rails #{rails_version}.").italic %> 33 | 34 | <% incompatible_gems_by_state[:incompatible].each do |gem| -%> 35 | <%= gem_header(gem) %> - new version, <%= gem.latest_version.version %>, is not compatible with Rails #{rails_version} 36 | <% end -%> 37 | 38 | <% end -%> 39 | <% if incompatible_gems_by_state[:no_new_version] -%> 40 | <%= Rainbow("=> Incompatible with Rails #{rails_version} (with no new versions):").white.bold %> 41 | <%= Rainbow("These gems will need to be upgraded by us or removed before upgrading to Rails #{rails_version}.").italic %> 42 | <%= Rainbow("This list is likely to contain internal gems, like Cuddlefish.").italic %> 43 | 44 | <% incompatible_gems_by_state[:no_new_version].each do |gem| -%> 45 | <%= gem_header(gem) %> - new version not found 46 | <% end -%> 47 | 48 | <% end -%> 49 | <%= Rainbow(incompatible_gems.length.to_s).red %> gems incompatible with Rails <%= rails_version %> 50 | ERB 51 | 52 | puts ERB.new(template, nil, "-").result(binding) 53 | end 54 | 55 | def self.gem_header(_gem) 56 | header = Rainbow("#{_gem.name} #{_gem.version}").bold 57 | header << Rainbow(" (loaded from git)").magenta if _gem.sourced_from_git? 58 | header 59 | end 60 | 61 | def self.outdated 62 | gems = TenYearsRails::GemInfo.all 63 | out_of_date_gems = gems.reject(&:up_to_date?).sort_by(&:created_at) 64 | percentage_out_of_date = ((out_of_date_gems.count / gems.count.to_f) * 100).round 65 | sourced_from_git = gems.select(&:sourced_from_git?) 66 | 67 | out_of_date_gems.each do |_gem| 68 | header = "#{_gem.name} #{_gem.version}" 69 | 70 | puts <<~MESSAGE 71 | #{Rainbow(header).bold.white}: released #{_gem.age} (latest version, #{_gem.latest_version.version}, released #{_gem.latest_version.age}) 72 | MESSAGE 73 | end 74 | 75 | puts "" 76 | puts <<~MESSAGE 77 | #{Rainbow(sourced_from_git.count.to_s).yellow} gems are sourced from git 78 | #{Rainbow(out_of_date_gems.length.to_s).red} of the #{gems.count} gems are out-of-date (#{percentage_out_of_date}%) 79 | MESSAGE 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /exe/deprecations: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "json" 3 | require "rainbow" 4 | require "optparse" 5 | require "set" 6 | 7 | def run_tests(deprecation_warnings, opts = {}) 8 | tracker_mode = opts[:tracker_mode] 9 | next_mode = opts[:next_mode] 10 | rspec_command = if next_mode 11 | "bin/next rspec" 12 | else 13 | "bundle exec rspec" 14 | end 15 | 16 | command = "DEPRECATION_TRACKER=#{tracker_mode} #{rspec_command} #{deprecation_warnings.keys.join(" ")}" 17 | puts command 18 | exec command 19 | end 20 | 21 | def print_info(deprecation_warnings, opts = {}) 22 | verbose = !!opts[:verbose] 23 | frequency_by_message = deprecation_warnings.each_with_object({}) do |(test_file, messages), hash| 24 | messages.each do |message| 25 | hash[message] ||= { test_files: Set.new, occurrences: 0 } 26 | hash[message][:test_files] << test_file 27 | hash[message][:occurrences] += 1 28 | end 29 | end.sort_by {|message, data| data[:occurrences] }.reverse.to_h 30 | 31 | puts Rainbow("Ten most common deprecation warnings:").underline 32 | frequency_by_message.take(10).each do |message, data| 33 | puts Rainbow("Occurrences: #{data.fetch(:occurrences)}").bold 34 | puts "Test files: #{data.fetch(:test_files).to_a.join(" ")}" if verbose 35 | puts Rainbow(message).red 36 | puts "----------" 37 | end 38 | end 39 | 40 | options = {} 41 | option_parser = OptionParser.new do |opts| 42 | opts.banner = <<-MESSAGE 43 | Usage: #{__FILE__.to_s} [options] [mode] 44 | 45 | Parses the deprecation warning shitlist and show info or run tests. 46 | 47 | Examples: 48 | bin/deprecations info # Show top ten deprecations 49 | bin/deprecations --next info # Show top ten deprecations for Rails 5 50 | bin/deprecations --pattern "ActiveRecord::Base" --verbose info # Show full details on deprecations matching pattern 51 | bin/deprecations --tracker-mode save --pattern "pass" run # Run tests that output deprecations matching pattern and update shitlist 52 | 53 | Modes: 54 | info 55 | Show information on the ten most frequent deprceation warnings. 56 | 57 | run 58 | Run tests that are known to cause deprecation warnings. Use --pattern to filter what tests are run. 59 | 60 | Options: 61 | MESSAGE 62 | 63 | opts.on("--next", "Run against the next shitlist") do |next_mode| 64 | options[:next] = next_mode 65 | end 66 | 67 | opts.on("--tracker-mode MODE", "Set DEPRECATION_TRACKER in test mode. Options: save or compare") do |tracker_mode| 68 | options[:tracker_mode] = tracker_mode 69 | end 70 | 71 | opts.on("--pattern RUBY_REGEX", "Filter deprecation warnings with a pattern.") do |pattern| 72 | options[:pattern] = pattern 73 | end 74 | 75 | opts.on("--verbose", "show more information") do 76 | options[:verbose] = true 77 | end 78 | 79 | opts.on_tail("-h", "--help", "Prints this help") do 80 | puts opts 81 | exit 82 | end 83 | end 84 | 85 | option_parser.parse! 86 | 87 | options[:mode] = ARGV.last 88 | path = options[:next] ? "spec/support/deprecation_warning.next.shitlist.json" : "spec/support/deprecation_warning.shitlist.json" 89 | 90 | pattern_string = options.fetch(:pattern, ".+") 91 | pattern = /#{pattern_string}/ 92 | 93 | deprecation_warnings = JSON.parse(File.read(path)).each_with_object({}) do |(test_file, messages), hash| 94 | filtered_messages = messages.select {|message| message.match(pattern) } 95 | hash[test_file] = filtered_messages if !filtered_messages.empty? 96 | end 97 | 98 | if deprecation_warnings.empty? 99 | abort "No test files with deprecations matching #{pattern.inspect}." 100 | exit 2 101 | end 102 | 103 | case options.fetch(:mode, "info") 104 | when "run" then run_tests(deprecation_warnings, next_mode: options[:next], tracker_mode: options[:tracker_mode]) 105 | when "info" then print_info(deprecation_warnings, verbose: options[:verbose]) 106 | when nil 107 | STDERR.puts Rainbow("Must pass a mode: run or info").red 108 | puts option_parser 109 | exit 1 110 | else 111 | STDERR.puts Rainbow("Unknown mode: #{options[:mode]}").red 112 | exit 1 113 | end 114 | -------------------------------------------------------------------------------- /lib/deprecation_tracker.rb: -------------------------------------------------------------------------------- 1 | require "rainbow" 2 | require "json" 3 | 4 | # A shitlist for deprecation warnings during test runs. It has two modes: "save" and "compare" 5 | # 6 | # DEPRECATION_TRACKER=save 7 | # Record deprecation warnings, grouped by spec file. After the test run, save to a file. 8 | # 9 | # DEPRECATION_TRACKER=compare 10 | # Tracks deprecation warnings, grouped by spec file. After the test run, compare against shitlist of expected 11 | # deprecation warnings. If anything is added or removed, raise an error with a diff of the changes. 12 | # 13 | class DeprecationTracker 14 | UnexpectedDeprecations = Class.new(StandardError) 15 | 16 | module KernelWarnTracker 17 | def self.callbacks 18 | @callbacks ||= [] 19 | end 20 | 21 | def warn(*messages) 22 | KernelWarnTracker.callbacks.each do |callback| 23 | messages.each { |message| callback.(message) } 24 | end 25 | 26 | super 27 | end 28 | end 29 | 30 | # There are two forms of the `warn` method: one for class Kernel and one for instances of Kernel (i.e., every Object) 31 | Object.prepend(KernelWarnTracker) 32 | 33 | # Ruby 2.2 and lower doesn't appear to allow overriding of Kernel.warn using `singleton_class.prepend`. 34 | if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3.0") 35 | Kernel.singleton_class.prepend(KernelWarnTracker) 36 | else 37 | def Kernel.warn(*args, &block) 38 | Object.warn(*args, &block) 39 | end 40 | end 41 | 42 | def self.track_rspec(rspec_config, opts = {}) 43 | shitlist_path = opts[:shitlist_path] 44 | mode = opts[:mode] 45 | transform_message = opts[:transform_message] 46 | deprecation_tracker = DeprecationTracker.new(shitlist_path, transform_message) 47 | if defined?(ActiveSupport) 48 | ActiveSupport::Deprecation.behavior << -> (message, _callstack, _deprecation_horizon, _gem_name) { deprecation_tracker.add(message) } 49 | end 50 | KernelWarnTracker.callbacks << -> (message) { deprecation_tracker.add(message) } 51 | 52 | rspec_config.around do |example| 53 | deprecation_tracker.bucket = example.metadata.fetch(:rerun_file_path) 54 | 55 | begin 56 | example.run 57 | ensure 58 | deprecation_tracker.bucket = nil 59 | end 60 | end 61 | 62 | rspec_config.after(:suite) do 63 | if mode == "save" 64 | deprecation_tracker.save 65 | elsif mode == "compare" 66 | deprecation_tracker.compare 67 | end 68 | end 69 | end 70 | 71 | attr_reader :deprecation_messages, :shitlist_path, :transform_message 72 | attr_reader :bucket 73 | 74 | def initialize(shitlist_path, transform_message = nil) 75 | @shitlist_path = shitlist_path 76 | @transform_message = transform_message || -> (message) { message } 77 | @deprecation_messages = {} 78 | end 79 | 80 | def add(message) 81 | return if bucket.nil? 82 | 83 | @deprecation_messages[bucket] << transform_message.(message) 84 | end 85 | 86 | def bucket=(value) 87 | @bucket = value 88 | @deprecation_messages[value] ||= [] unless value.nil? 89 | end 90 | 91 | def compare 92 | shitlist = read_shitlist 93 | 94 | changed_buckets = [] 95 | normalized_deprecation_messages.each do |bucket, messages| 96 | if shitlist[bucket] != messages 97 | changed_buckets << bucket 98 | end 99 | end 100 | 101 | if changed_buckets.length > 0 102 | message = Rainbow(<<-MESSAGE).red 103 | ⚠️ Deprecation warnings have changed! 104 | 105 | Code called by the following spec files is now generating different deprecation warnings: 106 | 107 | #{changed_buckets.join("\n")} 108 | 109 | To check your failures locally, you can run: 110 | 111 | DEPRECATION_TRACKER=compare bundle exec rspec #{changed_buckets.join(" ")} 112 | 113 | Here is a diff between what is expected and what was generated by this process: 114 | 115 | #{diff} 116 | 117 | See \e[4;37mdev-docs/testing/deprecation_tracker.md\e[0;31m for more information. 118 | MESSAGE 119 | 120 | raise UnexpectedDeprecations, message 121 | end 122 | end 123 | 124 | def diff 125 | new_shitlist = create_temp_shitlist 126 | `git diff --no-index #{shitlist_path} #{new_shitlist.path}` 127 | ensure 128 | new_shitlist.delete 129 | end 130 | 131 | def save 132 | new_shitlist = create_temp_shitlist 133 | FileUtils.cp(new_shitlist.path, shitlist_path) 134 | ensure 135 | new_shitlist.delete if new_shitlist 136 | end 137 | 138 | def create_temp_shitlist 139 | temp_file = Tempfile.new("temp-deprecation-tracker-shitlist") 140 | temp_file.write(JSON.pretty_generate(normalized_deprecation_messages)) 141 | temp_file.flush 142 | 143 | temp_file 144 | end 145 | 146 | # Normalize deprecation messages to reduce noise from file output and test files to be tracked with separate test runs 147 | def normalized_deprecation_messages 148 | normalized = read_shitlist.merge(deprecation_messages).each_with_object({}) do |(bucket, messages), hash| 149 | hash[bucket] = messages.sort 150 | end 151 | 152 | normalized.reject {|_key, value| value.empty? }.sort_by {|key, _value| key }.to_h 153 | end 154 | 155 | def read_shitlist 156 | return {} unless File.exist?(shitlist_path) 157 | JSON.parse(File.read(shitlist_path)) 158 | rescue JSON::ParserError => e 159 | raise "#{shitlist_path} is not valid JSON: #{e.message}" 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /spec/deprecation_tracker_spec.rb: -------------------------------------------------------------------------------- 1 | require "tempfile" 2 | require_relative "spec_helper" 3 | require_relative "../lib/deprecation_tracker" 4 | 5 | RSpec.describe DeprecationTracker do 6 | let(:shitlist_path) do 7 | shitlist_path = Tempfile.new("tmp").path 8 | FileUtils.rm(shitlist_path) 9 | shitlist_path 10 | end 11 | 12 | describe "#add" do 13 | it "groups messages by bucket" do 14 | subject = DeprecationTracker.new("/tmp/foo.txt") 15 | 16 | subject.bucket = "bucket 1" 17 | subject.add("error 1") 18 | subject.add("error 2") 19 | 20 | subject.bucket = "bucket 2" 21 | subject.add("error 3") 22 | subject.add("error 4") 23 | 24 | expect(subject.deprecation_messages).to eq( 25 | "bucket 1" => ["error 1", "error 2"], 26 | "bucket 2" => ["error 3", "error 4"] 27 | ) 28 | end 29 | 30 | it "ignores messages when bucket null" do 31 | subject = DeprecationTracker.new("/tmp/foo.txt") 32 | 33 | subject.bucket = nil 34 | subject.add("error 1") 35 | subject.add("error 2") 36 | 37 | expect(subject.deprecation_messages).to eq({}) 38 | end 39 | 40 | it "transforms messages before adding them" do 41 | subject = DeprecationTracker.new("/tmp/foo.txt", -> (message) { message + " foo" }) 42 | 43 | subject.bucket = "bucket 1" 44 | subject.add("a") 45 | 46 | expect(subject.deprecation_messages).to eq( 47 | "bucket 1" => ["a foo"] 48 | ) 49 | end 50 | end 51 | 52 | describe "#compare" do 53 | it "ignores buckets that have no messages" do 54 | setup_tracker = DeprecationTracker.new(shitlist_path) 55 | setup_tracker.bucket = "bucket 1" 56 | setup_tracker.add("a") 57 | setup_tracker.bucket = "bucket 2" 58 | setup_tracker.add("a") 59 | setup_tracker.save 60 | 61 | subject = DeprecationTracker.new(shitlist_path) 62 | 63 | subject.bucket = "bucket 2" 64 | subject.add("a") 65 | 66 | expect { subject.compare }.not_to raise_error 67 | end 68 | 69 | it "raises an error when recorded messages are different for a given bucket" do 70 | setup_tracker = DeprecationTracker.new(shitlist_path) 71 | setup_tracker.bucket = "bucket 1" 72 | setup_tracker.add("a") 73 | setup_tracker.save 74 | 75 | subject = DeprecationTracker.new(shitlist_path) 76 | 77 | subject.bucket = "bucket 1" 78 | subject.add("b") 79 | 80 | expect { subject.compare }.to raise_error(DeprecationTracker::UnexpectedDeprecations, /Deprecation warnings have changed/) 81 | end 82 | end 83 | 84 | describe "#save" do 85 | it "saves to disk" do 86 | subject = DeprecationTracker.new(shitlist_path) 87 | 88 | subject.bucket = "bucket 1" 89 | subject.add("b") 90 | subject.add("b") 91 | subject.add("a") 92 | 93 | subject.save 94 | 95 | expected_json = <<-JSON.chomp 96 | { 97 | "bucket 1": [ 98 | "a", 99 | "b", 100 | "b" 101 | ] 102 | } 103 | JSON 104 | expect(File.read(shitlist_path)).to eq(expected_json) 105 | end 106 | 107 | it "combines recorded and stored messages" do 108 | setup_tracker = DeprecationTracker.new(shitlist_path) 109 | setup_tracker.bucket = "bucket 1" 110 | setup_tracker.add("a") 111 | setup_tracker.save 112 | 113 | subject = DeprecationTracker.new(shitlist_path) 114 | 115 | subject.bucket = "bucket 2" 116 | subject.add("a") 117 | subject.save 118 | 119 | expected_json = <<-JSON.chomp 120 | { 121 | "bucket 1": [ 122 | "a" 123 | ], 124 | "bucket 2": [ 125 | "a" 126 | ] 127 | } 128 | JSON 129 | expect(File.read(shitlist_path)).to eq(expected_json) 130 | end 131 | 132 | it "overwrites stored messages with recorded messages with the same bucket" do 133 | setup_tracker = DeprecationTracker.new(shitlist_path) 134 | setup_tracker.bucket = "bucket 1" 135 | setup_tracker.add("a") 136 | setup_tracker.save 137 | 138 | subject = DeprecationTracker.new(shitlist_path) 139 | 140 | subject.bucket = "bucket 1" 141 | subject.add("b") 142 | subject.save 143 | 144 | expected_json = <<-JSON.chomp 145 | { 146 | "bucket 1": [ 147 | "b" 148 | ] 149 | } 150 | JSON 151 | expect(File.read(shitlist_path)).to eq(expected_json) 152 | end 153 | 154 | it "sorts by bucket" do 155 | subject = DeprecationTracker.new(shitlist_path) 156 | subject.bucket = "bucket 2" 157 | subject.add("a") 158 | subject.bucket = "bucket 1" 159 | subject.add("a") 160 | subject.save 161 | 162 | expected_json = <<-JSON.chomp 163 | { 164 | "bucket 1": [ 165 | "a" 166 | ], 167 | "bucket 2": [ 168 | "a" 169 | ] 170 | } 171 | JSON 172 | expect(File.read(shitlist_path)).to eq(expected_json) 173 | end 174 | 175 | it "sorts messages" do 176 | subject = DeprecationTracker.new(shitlist_path) 177 | subject.bucket = "bucket 1" 178 | subject.add("b") 179 | subject.add("c") 180 | subject.add("a") 181 | subject.save 182 | 183 | expected_json = <<-JSON.chomp 184 | { 185 | "bucket 1": [ 186 | "a", 187 | "b", 188 | "c" 189 | ] 190 | } 191 | JSON 192 | expect(File.read(shitlist_path)).to eq(expected_json) 193 | end 194 | end 195 | 196 | describe DeprecationTracker::KernelWarnTracker do 197 | it "captures Kernel#warn" do 198 | warn_messages = [] 199 | DeprecationTracker::KernelWarnTracker.callbacks << -> (message) { warn_messages << message } 200 | 201 | expect do 202 | Kernel.warn "oh" 203 | Kernel.warn "no" 204 | end.to output("oh\nno\n").to_stderr 205 | 206 | expect(warn_messages).to eq(["oh", "no"]) 207 | end 208 | 209 | it "captures Kernel.warn" do 210 | warn_messages = [] 211 | DeprecationTracker::KernelWarnTracker.callbacks << -> (message) { warn_messages << message } 212 | 213 | expect do 214 | Kernel.warn "oh" 215 | Kernel.warn "no" 216 | end.to output("oh\nno\n").to_stderr 217 | 218 | expect(warn_messages).to eq(["oh", "no"]) 219 | end 220 | end 221 | end 222 | -------------------------------------------------------------------------------- /deprecation_tracker.md: -------------------------------------------------------------------------------- 1 | # Deprecation Tracker 2 | 3 | In order to control the deprecation warnings that occur during a test run, we create a shitlist of deprecation warnings that we expect to see for each spec file. If code run by a spec file changes how often or what deprecation warning occurs, an error is raised after the RSpec run is complete. 4 | 5 | It looks something like this: 6 | 7 | ``` 8 | An error occurred in an `after(:suite)` hook. 9 | Failure/Error: raise UnexpectedDeprecations, message 10 | 11 | DeprecationTracker::UnexpectedDeprecations: 12 | ⚠️ Deprecation warnings have changed! 13 | 14 | Code called by the following spec files is now generating different deprecation warnings: 15 | 16 | ./spec/deprecation_spec.rb 17 | 18 | Here is a diff between what is expected and what was generated by this process: 19 | 20 | diff --git a/spec/support/deprecation_tracker.json b/var/folders/mv/x81dlrp92w5053_m9bgqbkmm0000gp/T/test-deprecations20180328-33449-xgor20 21 | index 76d2118f9f2..257be30d46c 100644 22 | --- a/spec/support/deprecation_tracker.json 23 | +++ b/var/folders/mv/x81dlrp92w5053_m9bgqbkmm0000gp/T/test-deprecations20180328-33449-xgor20 24 | @@ -1,7 +1,8 @@ 25 | { 26 | "./spec/deprecation_spec.rb": [ 27 | "DEPRECATION WARNING: `ActiveRecord::Base.symbolized_base_class` is deprecated and will be removed without replacement. (called from block (2 levels) in at /Users/Jordan/projects/themis3/spec/deprecation_spec.rb:5)", 28 | - "DEPRECATION WARNING: `ActiveRecord::Base.symbolized_base_class` is deprecated and will be removed without replacement. (called from block (2 levels) in at /Users/Jordan/projects/themis3/spec/deprecation_spec.rb:6)" 29 | + "DEPRECATION WARNING: `ActiveRecord::Base.symbolized_base_class` is deprecated and will be removed without replacement. (called from block (2 levels) in at /Users/Jordan/projects/themis3/spec/deprecation_spec.rb:6)", 30 | + "DEPRECATION WARNING: `ActiveRecord::Base.symbolized_base_class` is deprecated and will be removed without replacement. (called from block (2 levels) in at /Users/Jordan/projects/themis3/spec/deprecation_spec.rb:7)" 31 | ], 32 | "./spec/models/refund_spec.rb": [ 33 | 34 | # ./spec/support/deprecation_tracker.rb:65:in `compare' 35 | # ./spec/support/deprecation_tracker.rb:29:in `block in track_rspec' 36 | # /Users/Jordan/.rbenv/versions/2.4.3/bin/rspec:23:in `load' 37 | # /Users/Jordan/.rbenv/versions/2.4.3/bin/rspec:23:in `' 38 | # /Users/Jordan/.rbenv/versions/2.4.3/bin/bundle:23:in `load' 39 | # /Users/Jordan/.rbenv/versions/2.4.3/bin/bundle:23:in `
' 40 | 41 | Finished in 1.83 seconds (files took 9.09 seconds to load) 42 | 1 example, 0 failures, 1 error occurred outside of examples 43 | ``` 44 | 45 | When the diff shows a line was removed, it means we expected to see that deprecation message but didn't. 46 | When the diff shows a line was added, it means didn't expect to see that deprecation message. 47 | 48 | > **Protip**: If you find the diff difficult to read in your terminal, copy/paste it into your text editor and set syntax highlighting to "diff" for a colorized view. 49 | 50 | Keeping a list of all deprecation warnings has two primary benefits: 51 | 52 | - We can fail CI when new deprecation warnings are added (i.e., "stop the bleeding") 53 | - We can more easily find and eliminate deprecation warnings 54 | 55 | ## Modes 56 | 57 | The deprecation tracker has three mode: `compare`, `save`, and off. 58 | 59 | - `DEPRECATION_TRACKER=compare rspec foo_spec.rb`: in `compare` mode, changes to the deprecation warnings output by a test file will trigger an error. 60 | - `DEPRECATION_TRACKER=save rspec foo_spec.rb`: in `save` mode, changes to the deprecation warnings output by a test file will update the shitlist. 61 | - `rspec foo_spec.rb`: when turned off, changes to the deprecation warnings output by a test file won't trigger a warning or update the shitlist. 62 | 63 | 64 | ## What does it track? 65 | 66 | This tracks deprecations from Rails and `Kernel#warn`, the latter often used by gems that don't use ActiveSupport. 67 | 68 | It only tracks deprecation warnings that occur during a test and not before or after. This means that some deprecations, for example the ones you might see while the app boots, won't be tracked. 69 | 70 | It also doesn't track constant deprecations (`warning: constant Foo is deprecated`) because those [happen in C code](http://ruby-doc.org/core-2.3.0/Module.html#method-i-deprecate_constant). (If you can think of a way to capture these, do it!) 71 | 72 | ## How do I get my pull request green on CI? 73 | 74 | ### There are _added_ deprecation warnings 75 | 76 | If the diff shows added deprecation warnings, you'll need to go back and change code until those messages don't happen. Under normal circumstances, we shouldn't increase the number of deprecation warnings. 77 | 78 | You can check if your test file has changed deprecation warnings by running this command: 79 | 80 | ```bash 81 | DEPRECATION_TRACKER=compare bundle exec rspec spec/foo_spec.rb 82 | ``` 83 | 84 | In this case, an error will be raised if `spec/foo_spec.rb` triggers deprecation warnings that are different than we expect. 85 | 86 | > An example of when it would be OK to increase the number of deprecation warnings is during a Rails upgrade. We're not introducing code that adds deprecation warnings but rather change libraries so what used to be safe to call is now deprecated. 87 | 88 | ### There are _removed_ deprecation warnings 89 | 90 | If the diff shows removed deprecation warnings, congratulations! Pat yourself on the back and then run this command: 91 | 92 | ```ruby 93 | DEPRECATION_TRACKER=save bundle exec rspec 94 | ``` 95 | 96 | This will rerun the tests that reduces the number of deprecation warnings and save that to the log. 97 | 98 | Be sure to commit your changes, then push and wait for CI to go ✅. 99 | 100 | ### The deprecation warnings are _different_ 101 | 102 | If the diff shows added and removed lines, the deprecation messages we expected to see may have changed. Most deprecation warnings contain the file and line of app code that caused it. If the line number changes, the deprecation warning message will also change. In this case, you can follow the same steps as you would if you removed deprecation warnings to update the stale deprecation messages: 103 | 104 | ```ruby 105 | DEPRECATION_TRACKER=save bundle exec rspec 106 | ``` 107 | 108 | This will update the deprecation warning shitlist. Be sure to commit your changes, then push and wait for CI to go ✅. 109 | 110 | ## Where is the shitlist stored? 111 | 112 | ``` 113 | spec/support/deprecation_warnings.shitlist.json 114 | ``` 115 | 116 | ## How does it work? 117 | 118 | While running tests, we keep track of what test file is being run and record each deprecation warning as it happens. After the test run is complete, we compare the deprecation messages we saw with the deprecation messages we expected to see and raise an error if the two differ. 119 | 120 | See `spec/support/deprecation_tracker.rb` for implementation details. 121 | --------------------------------------------------------------------------------