deprecation_toolkit 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.rubocop.yml ADDED
@@ -0,0 +1,5 @@
1
+ inherit_from:
2
+ - https://shopify.github.io/ruby-style-guide/rubocop.yml
3
+
4
+ AllCops:
5
+ TargetRubyVersion: 2.3
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ group :deployment do
8
+ gem 'rake'
9
+ gem 'rubocop'
10
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,53 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ deprecation_toolkit (1.0.0)
5
+ activesupport (>= 5.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activesupport (5.2.0)
11
+ concurrent-ruby (~> 1.0, >= 1.0.2)
12
+ i18n (>= 0.7, < 2)
13
+ minitest (~> 5.1)
14
+ tzinfo (~> 1.1)
15
+ ast (2.4.0)
16
+ concurrent-ruby (1.0.5)
17
+ i18n (1.0.1)
18
+ concurrent-ruby (~> 1.0)
19
+ jaro_winkler (1.5.1)
20
+ minitest (5.11.3)
21
+ parallel (1.12.1)
22
+ parser (2.5.1.0)
23
+ ast (~> 2.4.0)
24
+ powerpack (0.1.2)
25
+ rainbow (2.2.2)
26
+ rake
27
+ rake (10.5.0)
28
+ rubocop (0.58.0)
29
+ jaro_winkler (~> 1.5.1)
30
+ parallel (~> 1.10)
31
+ parser (>= 2.5)
32
+ powerpack (~> 0.1)
33
+ rainbow (>= 2.2.2, < 4.0)
34
+ ruby-progressbar (~> 1.7)
35
+ unicode-display_width (~> 1.0, >= 1.0.1)
36
+ ruby-progressbar (1.9.0)
37
+ thread_safe (0.3.6)
38
+ tzinfo (1.2.5)
39
+ thread_safe (~> 0.1)
40
+ unicode-display_width (1.4.0)
41
+
42
+ PLATFORMS
43
+ ruby
44
+
45
+ DEPENDENCIES
46
+ bundler (~> 1.16)
47
+ deprecation_toolkit!
48
+ minitest (~> 5.0)
49
+ rake
50
+ rubocop
51
+
52
+ BUNDLED WITH
53
+ 1.16.1
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Shopify
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.
data/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # ⚒ Deprecation Toolkit ⚒
2
+
3
+ ## Introduction
4
+
5
+ The Deprecation Toolkit is a gem to help you get rid of deprecations in your codebase.
6
+ Having deprecations in your application is usually a sign that something will break whenever a third dependency will get updated. The sooner the better to fix them!
7
+ Fixing all deprecations at once might be though depending on how big your app is and how much deprecations needs to be fixed. You might have to progressively resolve them while making sure your team doesn't add new one ➰. This is where this gem comes handy!
8
+
9
+
10
+ ## How it works
11
+
12
+ The Deprecation Toolkit gem works by using a [shitlist approach](https://confreaks.tv/videos/reddotrubyconf2017-shitlist-driven-development-and-other-tricks-for-working-on-large-codebases).
13
+ First, all current existing deprecations in your codebase are recorded into `.yml` files. When running a test that has non-recorded deprecations, the Deprecation Toolkit gem will trigger a behavior of your choice (by default raise an error).
14
+
15
+ ## Recording Deprecations
16
+
17
+ As said above, the Deprecation Toolkit works by using a shitlist approach. You have two ways to record deprecations.
18
+ Either set `DeprecationToolkit::Configuration.behavior` to `DeprecationToolkit::Behaviors::Record` (see the Configuration Reference below)
19
+ Or run your tests with the `--record-deprecations` flag (or simply the `-r` shortcut)
20
+ ```sh
21
+ rails test <path_to_my_test.rb> -r
22
+ ```
23
+
24
+ ## Configuration Reference
25
+
26
+ ### 🔨 `#DeprecationToolkit::Configuration#deprecation_path`
27
+
28
+ You can control where the recorded deprecations are read and write into. By default, deprecations will be recorded in the `test/deprecations` folder.
29
+
30
+ The `deprecation_path` either accepts a string or a proc. When using a proc, the proc will be passed an argument which is the path of the test file being run.
31
+
32
+ ```ruby
33
+ DeprecationToolkit::Configuration.deprecation_path = 'test/deprecations'
34
+ DeprecationToolkit::Configuration.deprecation_path = -> (test_location) do
35
+ if test_location == 'admin_test.rb'
36
+ 'test/deprecations/admin'
37
+ else
38
+ 'test/deprecations/storefront'
39
+ end
40
+ end
41
+ ```
42
+
43
+ ### 🔨 `#DeprecationToolkit::Configuration#behavior`
44
+
45
+ Behaviors defines what will happen when a non-recorded deprecations is encountered.
46
+
47
+ Behaviors are class that responds to the `trigger` message.
48
+
49
+ This gem provides 3 behaviors, the default one being `DeprecationToolkit::Behaviors::Raise`.
50
+
51
+ * `DeprecationToolkit::Behaviors::Raise` will raise either:
52
+ - `DeprecationToolkit::DeprecationIntroduced` error if a new deprecation is introduced.
53
+ - `DeprecationToolkit::DeprecationRemoved` error if a deprecation was removed (compare to the one recorded in the shitlist).
54
+ * `DeprecationToolkit::Behaviors::Record` will record deprecations.
55
+ * `DeprecationToolkit::Behaviors::Disabled` will do nothing.
56
+ - This is useful if you want to disable this gem for a moment without removing the gem from your Gemfile.
57
+
58
+ ```ruby
59
+ DeprecationToolkit::Configuration.behavior = DeprecationToolkit::Behaviors::Record
60
+ ```
61
+
62
+ You can also create your own behavior class and perform the logic you want. Your behavior needs to respond to the `.trigger` message.
63
+
64
+ ```ruby
65
+ class StatsdBehavior
66
+ def self.trigger(test, deprecations, recorded_deprecations)
67
+ # Could send some statsd event for example
68
+ end
69
+ end
70
+
71
+ DeprecationToolkit::Configuration.behavior = StatsdBehavior
72
+ ```
73
+
74
+ ### 🔨 `#DeprecationToolkit::Configuration#allowed_deprecations`
75
+
76
+ If you want to allow some deprecations, this is where you'll configure it. The `allowed_deprecations` configuration accepts an
77
+ array of Regexp.
78
+
79
+ Whenever a deprecation matches one of the regex, the deprecation will be ignored
80
+
81
+ ```ruby
82
+ DeprecationToolkit::Configuration.allowed_deprecations = [/Hello World/]
83
+
84
+ # Let's imagine a third dependency adds a deprecation like this,
85
+ # the Deprecation Toolkit will simply ignore it.
86
+ ActiveSupport::Deprecation.warn('Hello World')
87
+ ```
88
+
89
+ ### 🔨 `#DeprecationToolkit::Configuration#warnings_treated_as_deprecation`
90
+
91
+ Most gems doesn't use `ActiveSupport::Deprecation` to deprecate their code but instead just uses `Kernel#warn` to output
92
+ a message in the console.
93
+
94
+ The DeprecationToolkit gem allows you to configure which warnings should be treated as deprecations in order for you
95
+ to keep track of them as if they were regular deprecations.
96
+
97
+ ## License
98
+
99
+ Deprecation Toolkit is lincensed under the [MIT license](LICENSE.txt).
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.warning = true
10
+ t.test_files = FileList["test/**/*_test.rb"]
11
+ end
12
+
13
+ task default: :test
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("lib", __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "deprecation_toolkit/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "deprecation_toolkit"
9
+ spec.version = DeprecationToolkit::VERSION
10
+ spec.authors = %w(Shopify)
11
+ spec.email = ["rails@shopify.com"]
12
+
13
+ spec.summary = "Deprecation Toolkit around ActiveSupport::Deprecation"
14
+ spec.homepage = "https://github.com/shopify/deprecation_toolkit"
15
+ spec.license = "MIT"
16
+
17
+ spec.required_ruby_version = '>= 2.3'
18
+
19
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
20
+ f.match(%r{^(test)/})
21
+ end
22
+ spec.require_paths = %w(lib)
23
+
24
+ spec.add_runtime_dependency 'activesupport', '>= 5.0'
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.16"
27
+ spec.add_development_dependency "rake", "~> 10.0"
28
+ spec.add_development_dependency "minitest", "~> 5.0"
29
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeprecationToolkit
4
+ module Behaviors
5
+ class Disabled
6
+ def self.trigger(*)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeprecationToolkit
4
+ module Behaviors
5
+ class Raise
6
+ def self.trigger(_test, current_deprecations, recorded_deprecations)
7
+ error_class = if current_deprecations > recorded_deprecations
8
+ DeprecationIntroduced
9
+ else
10
+ DeprecationRemoved
11
+ end
12
+
13
+ raise error_class.new(current_deprecations, recorded_deprecations)
14
+ end
15
+ end
16
+
17
+ DeprecationException = Class.new(StandardError)
18
+
19
+ class DeprecationIntroduced < DeprecationException
20
+ def initialize(current_deprecations, recorded_deprecations)
21
+ introduced_deprecations = current_deprecations - recorded_deprecations
22
+
23
+ message = <<~EOM
24
+ You have introduced new deprecations in the codebase. Fix or record them in order to discard this error.
25
+ You can record deprecations by adding the `--record-deprecations` flag when running your tests.
26
+
27
+ #{introduced_deprecations.join("\n")}
28
+ EOM
29
+
30
+ super(message)
31
+ end
32
+ end
33
+
34
+ class DeprecationRemoved < DeprecationException
35
+ def initialize(current_deprecations, recorded_deprecations)
36
+ removed_deprecations = recorded_deprecations - current_deprecations
37
+
38
+ message = <<~EOM
39
+ You have removed deprecations from the codebase. Thanks for being an awesome person.
40
+ The recorded deprecations needs to be updated to reflect your changes.
41
+ You can re-record deprecations by adding the `--record-deprecations` flag when running your tests.
42
+
43
+ #{removed_deprecations.join("\n")}
44
+ EOM
45
+
46
+ super(message)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeprecationToolkit
4
+ module Behaviors
5
+ class Record
6
+ extend ReadWriteHelper
7
+
8
+ def self.trigger(test, collector, _)
9
+ write(test, collector.deprecations_without_stacktrace)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/class/attribute"
4
+
5
+ module DeprecationToolkit
6
+ class Collector
7
+ include Comparable
8
+ extend ReadWriteHelper
9
+
10
+ class_attribute :deprecations
11
+ self.deprecations = []
12
+
13
+ class << self
14
+ def collect(message)
15
+ deprecations << message
16
+ end
17
+
18
+ def load(test)
19
+ new(read(test))
20
+ end
21
+
22
+ def reset!
23
+ deprecations.clear
24
+ end
25
+ end
26
+
27
+ def initialize(deprecations)
28
+ self.deprecations = deprecations
29
+ end
30
+
31
+ def <=>(other)
32
+ deprecations_without_stacktrace <=> other.deprecations_without_stacktrace
33
+ end
34
+
35
+ def deprecations_without_stacktrace
36
+ deprecations.map { |deprecation| deprecation.sub(/ \(called from .*\)$/, "") }
37
+ end
38
+
39
+ def -(other)
40
+ difference = deprecations.dup
41
+ current = deprecations_without_stacktrace
42
+ other = other.deprecations_without_stacktrace
43
+
44
+ other.each do |deprecation|
45
+ index = current.index(deprecation)
46
+
47
+ if index
48
+ current.delete_at(index)
49
+ difference.delete_at(index)
50
+ end
51
+ end
52
+
53
+ difference
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/configurable"
4
+
5
+ module DeprecationToolkit
6
+ class Configuration
7
+ include ActiveSupport::Configurable
8
+
9
+ config_accessor(:behavior) { Behaviors::Raise }
10
+ config_accessor(:allowed_deprecations) { [] }
11
+ config_accessor(:deprecation_path) { "test/deprecations" }
12
+ config_accessor(:attach_to) { [:rails] }
13
+ config_accessor(:warnings_treated_as_deprecation) { [] }
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/subscriber"
4
+
5
+ module DeprecationToolkit
6
+ class DeprecationSubscriber < ActiveSupport::Subscriber
7
+ def deprecation(event)
8
+ message = event.payload[:message]
9
+
10
+ Collector.collect(message) unless deprecation_allowed?(message)
11
+ end
12
+
13
+ private
14
+
15
+ def deprecation_allowed?(message)
16
+ Configuration.allowed_deprecations.any? { |regex| regex =~ message }
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest"
4
+
5
+ module DeprecationToolkit
6
+ module Minitest
7
+ def trigger_deprecation_toolkit_behavior
8
+ current_deprecations = Collector.new(Collector.deprecations)
9
+ recorded_deprecations = Collector.load(self)
10
+ if current_deprecations != recorded_deprecations
11
+ Configuration.behavior.trigger(self, current_deprecations, recorded_deprecations)
12
+ end
13
+ ensure
14
+ Collector.reset!
15
+ end
16
+ end
17
+ end
18
+
19
+ module Minitest
20
+ class Test
21
+ include DeprecationToolkit::Minitest
22
+
23
+ TEARDOWN_METHODS << "trigger_deprecation_toolkit_behavior"
24
+ end
25
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/inflections"
4
+ require "yaml"
5
+
6
+ module DeprecationToolkit
7
+ module ReadWriteHelper
8
+ def read(test)
9
+ deprecation_file = recorded_deprecations_path(test)
10
+ YAML.load(deprecation_file.read).fetch(test.name, [])
11
+ rescue Errno::ENOENT
12
+ []
13
+ end
14
+
15
+ def write(test, deprecations)
16
+ deprecation_file = recorded_deprecations_path(test)
17
+ create_deprecation_file(deprecation_file) unless deprecation_file.exist?
18
+
19
+ content = YAML.load_file(deprecation_file)
20
+ if deprecations.any?
21
+ content[test.name] = deprecations
22
+ else
23
+ content.delete(test.name)
24
+ end
25
+
26
+ if content.any?
27
+ deprecation_file.write(YAML.dump(content))
28
+ else
29
+ deprecation_file.delete
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def create_deprecation_file(deprecation_file)
36
+ deprecation_file.dirname.mkpath
37
+ deprecation_file.write(YAML.dump({}))
38
+ end
39
+
40
+ def recorded_deprecations_path(test)
41
+ deprecation_folder = if Configuration.deprecation_path.is_a?(Proc)
42
+ Configuration.deprecation_path.call(test_location(test))
43
+ else
44
+ Configuration.deprecation_path
45
+ end
46
+
47
+ Bundler.root.join(deprecation_folder, "#{test.class.name.underscore}.yml")
48
+ end
49
+
50
+ def test_location(test)
51
+ test.method(test.name).source_location[0]
52
+ rescue NameError
53
+ "unknown"
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeprecationToolkit
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeprecationToolkit
4
+ module Warning
5
+ def warn(str)
6
+ if DeprecationToolkit::Configuration.warnings_treated_as_deprecation.any? { |warning| warning =~ str }
7
+ ActiveSupport::Deprecation.warn(str)
8
+ else
9
+ super
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ Warning.singleton_class.prepend(DeprecationToolkit::Warning)
16
+
17
+ # https://bugs.ruby-lang.org/issues/12944
18
+ if RUBY_VERSION <= '2.5.0' && RUBY_ENGINE == 'ruby'
19
+ module Kernel
20
+ remove_method :warn
21
+
22
+ def warn(*messages)
23
+ Array(messages.flatten).each { |msg| Warning.warn(msg) }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeprecationToolkit
4
+ autoload :DeprecationSubscriber, "deprecation_toolkit/deprecation_subscriber"
5
+ autoload :Configuration, "deprecation_toolkit/configuration"
6
+ autoload :Collector, "deprecation_toolkit/collector"
7
+ autoload :ReadWriteHelper, "deprecation_toolkit/read_write_helper"
8
+
9
+ module Behaviors
10
+ autoload :Disabled, "deprecation_toolkit/behaviors/disabled"
11
+ autoload :Raise, "deprecation_toolkit/behaviors/raise"
12
+ autoload :Record, "deprecation_toolkit/behaviors/record"
13
+ end
14
+ end
15
+
16
+ require "deprecation_toolkit/minitest_hook"
17
+ require "deprecation_toolkit/warning"
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitest
4
+ extend self
5
+
6
+ def plugin_deprecation_toolkit_options(opts, options)
7
+ opts.on "-r", "--record-deprecations", "Record deprecations" do
8
+ options[:record_deprecations] = true
9
+ end
10
+ end
11
+
12
+ def plugin_deprecation_toolkit_init(options)
13
+ if options[:record_deprecations]
14
+ DeprecationToolkit::Configuration.behavior = DeprecationToolkit::Behaviors::Record
15
+ end
16
+
17
+ add_notify_behavior
18
+ attach_subscriber
19
+ end
20
+
21
+ private
22
+
23
+ def add_notify_behavior
24
+ notify = ActiveSupport::Deprecation::DEFAULT_BEHAVIORS[:notify]
25
+ behaviors = ActiveSupport::Deprecation.behavior
26
+
27
+ unless behaviors.find { |behavior| behavior == notify }
28
+ ActiveSupport::Deprecation.behavior = behaviors << notify
29
+ end
30
+ end
31
+
32
+ def attach_subscriber
33
+ DeprecationToolkit::Configuration.attach_to.each do |gem_name|
34
+ DeprecationToolkit::DeprecationSubscriber.attach_to gem_name
35
+ end
36
+ end
37
+ end
@@ -0,0 +1 @@
1
+ # using the default shipit config