climate_control 0.2.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 21d2f4ce1d74f8b2e4650cd7ab4efd3d40dac9d6
4
- data.tar.gz: d14c7f5bc3b4ae309682f529c0ada07a9a874d9b
2
+ SHA256:
3
+ metadata.gz: '00584062039c6b9161ff0e4a1b61ca51e4d8742225fc611180c4effcc43959c5'
4
+ data.tar.gz: 99135f45c57f58d87e17480896045c00eecebd632ed64eaf18bd1964ef7d454d
5
5
  SHA512:
6
- metadata.gz: 1c533460213a9e6a4285c3daa7fade227a4a5a5e4eb9a23e41d6bb06362c60468739476dd916be6b9ec3447b3e155dc1594e0fd89597ae58935acef27b850940
7
- data.tar.gz: 1a9aefaad845f0cdcb147b77cd176e81c0004d28fa17979aab312d27fe1e5251653902bc6fe05581ccd1ecab49075f122b7d029953c708af350e6ad0809f575d
6
+ metadata.gz: 01ba4ad3241cea796d408f0adb02f8c23b85771d1ce8338eedf973b95228a7cac9978218f08921915918c90a0f8f292dbb38f9401ca29de8d7a6ef3dc76d99b5
7
+ data.tar.gz: 9b20ce4affbcf22b6204016d31b7b54465ad5ba2a143fd2e6578d3dc5d4c80ac167c02c9ccc47d91c2a62441c5ce3037ebb13c445407876c163edaba73eb484d
@@ -0,0 +1,23 @@
1
+ name: CI
2
+ on: [pull_request]
3
+ jobs:
4
+ tests:
5
+ name: Tests
6
+ runs-on: ubuntu-latest
7
+ strategy:
8
+ matrix:
9
+ ruby-version: [3.1, '3.0', 2.7, 2.6, 2.5]
10
+ steps:
11
+ - uses: actions/checkout@v2
12
+ - name: Set up Ruby ${{ matrix.ruby-version }}
13
+ uses: ruby/setup-ruby@v1
14
+ with:
15
+ ruby-version: ${{ matrix.ruby-version }}
16
+ - name: Install gems
17
+ run: |
18
+ bundle config path vendor/bundle
19
+ bundle install --jobs 4 --retry 3
20
+ - name: Run tests
21
+ run: bundle exec rake
22
+ - name: Run StandardRB
23
+ run: bundle exec standardrb
data/CHANGELOG.md ADDED
@@ -0,0 +1,53 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## 1.2.0 / 2022-07-15
9
+
10
+ - Added: `ClimateControl.unsafe_modify` for a thread-unsafe version of
11
+ `ClimateControl.modify` (useful for minitest-around for instance)
12
+ - Deprecates `ClimateControl.env`, `ENV` should be used instead
13
+
14
+ ## 1.1.1 / 2022-05-28
15
+
16
+ - Fixed: ENV was not restored if an error was thrown when assigning ENV
17
+
18
+ ## 1.1.0 / 2022-05-26
19
+
20
+ - Refactor to use `Monitor` instead of `Mutex`
21
+ - Add documentation about thread-safety
22
+ - Allow ClimateControl.modify to be called without environment variables
23
+ - Add test for concurrent access needed to be inside block
24
+ - Relax development dependencies
25
+
26
+ ## 1.0.1 / 2021-05-26
27
+
28
+ - Require minimum Ruby version of 2.5.0
29
+
30
+ # 1.0.0 / 2021-03-06
31
+
32
+ - Commit to supporting latest patch versions of Ruby 2.5+
33
+ - Improve documentation
34
+ - Format code with StandardRB
35
+ - Bump gem dependencies
36
+
37
+ # 0.2.0 / 2017-05-12
38
+
39
+ - Allow nested environment changes in the same thread
40
+
41
+ # 0.1.0 / 2017-01-07
42
+
43
+ - Remove ActiveSupport dependency
44
+
45
+ # 0.0.4 / 2017-01-06
46
+
47
+ - Improved thread safety
48
+ - Handle TypeErrors during assignment
49
+ - Improve documentation
50
+
51
+ # 0.0.1 / 2012-11-28
52
+
53
+ - Initial release
data/CODEOWNERS ADDED
@@ -0,0 +1 @@
1
+ * @dorianmariefr
@@ -0,0 +1,6 @@
1
+ # Code of Conduct
2
+
3
+ By participating in this project, you agree to abide by the
4
+ [thoughtbot code of conduct][1].
5
+
6
+ [1]: https://thoughtbot.com/open-source-code-of-conduct
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012-2014 Joshua Clayton and thoughtbot, inc.
1
+ Copyright (c) 2012-2021 Joshua Clayton and thoughtbot, inc.
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Climate Control
2
2
 
3
+ ![GitHub Actions CI](https://github.com/thoughtbot/climate_control/actions/workflows/ci.yml/badge.svg)
4
+
3
5
  Easily manage your environment.
4
6
 
5
7
  ## Installation
@@ -24,15 +26,31 @@ within a block:
24
26
  ```ruby
25
27
  ClimateControl.modify CONFIRMATION_INSTRUCTIONS_BCC: 'confirmation_bcc@example.com' do
26
28
  sign_up_as 'john@example.com'
29
+
30
+ confirm_account_for_email 'john@example.com'
31
+
32
+ expect(current_email).to bcc_to('confirmation_bcc@example.com')
33
+ end
34
+ ```
35
+
36
+ To modify multiple environment variables:
37
+
38
+ ```ruby
39
+ ClimateControl.modify CONFIRMATION_INSTRUCTIONS_BCC: 'confirmation_bcc@example.com',
40
+ MAIL_FROM: 'us@example.com' do
41
+ sign_up_as 'john@example.com'
42
+
27
43
  confirm_account_for_email 'john@example.com'
28
- current_email.should bcc_to('confirmation_bcc@example.com')
44
+
45
+ expect(current_email).to bcc_to('confirmation_bcc@example.com')
46
+ expect(current_email).to be_from('us@example.com')
29
47
  end
30
48
  ```
31
49
 
32
50
  To use with RSpec, you could define this in your spec:
33
51
 
34
52
  ```ruby
35
- def with_modified_env(options, &block)
53
+ def with_modified_env(options = {}, &block)
36
54
  ClimateControl.modify(options, &block)
37
55
  end
38
56
  ```
@@ -89,6 +107,39 @@ manner becomes more difficult:
89
107
  Climate Control modifies environment variables only within the context of the
90
108
  block, ensuring values are managed properly and consistently.
91
109
 
110
+ ## Thread-safety
111
+
112
+ When using threads, for instance when running tests concurrently in the same
113
+ process, you may need to wrap your code inside `ClimateControl.modify` blocks,
114
+ e.g.:
115
+
116
+ ```ruby
117
+ first_thread = Thread.new do
118
+ ClimateControl.modify(SECRET: "1") do
119
+ p ENV["SECRET"] # => "1"
120
+ sleep 2
121
+ p ENV["SECRET"] # => "1"
122
+ end
123
+ end
124
+
125
+ second_thread = Thread.new do
126
+ ClimateControl.modify({}) do
127
+ sleep 1
128
+ p ENV["SECRET"] # => nil
129
+ sleep 1
130
+ p ENV["SECRET"] # => nil
131
+ end
132
+ end
133
+
134
+ first_thread.join
135
+ second_thread.join
136
+ ```
137
+
138
+ > The modification wraps ENV in a mutex. If there's contention (the env being used - including potentially mutating values), it blocks until the value is freed (we shift out of the Ruby block).
139
+ >
140
+ > <cite><a href="https://github.com/thoughtbot/climate_control/issues/32#issuecomment-800713686">Josh Clayton</a></cite>
141
+
142
+
92
143
  ## Contributing
93
144
 
94
145
  1. Fork it
@@ -97,6 +148,23 @@ block, ensuring values are managed properly and consistently.
97
148
  4. Push to the branch (`git push origin my-new-feature`)
98
149
  5. Create new Pull Request
99
150
 
151
+ This project uses [StandardRB](https://github.com/testdouble/standard) to ensure formatting.
152
+
100
153
  ## License
101
154
 
102
- climate_control is copyright 2012-2017 Joshua Clayton and thoughtbot, inc. It is free software and may be redistributed under the terms specified in the [LICENSE.txt](https://github.com/thoughtbot/climate_control/blob/master/LICENSE.txt) file.
155
+ climate_control is copyright 2012-2021 Joshua Clayton and thoughtbot, inc. It is free software and may be redistributed under the terms specified in the [LICENSE](https://github.com/thoughtbot/climate_control/blob/main/LICENSE) file.
156
+
157
+ About thoughtbot
158
+ ----------------
159
+
160
+ ![thoughtbot](https://thoughtbot.com/brand_assets/93:44.svg)
161
+
162
+ climate_control is maintained and funded by thoughtbot, inc.
163
+ The names and logos for thoughtbot are trademarks of thoughtbot, inc.
164
+
165
+ We love open source software!
166
+ See [our other projects][community] or
167
+ [hire us][hire] to design, develop, and grow your product.
168
+
169
+ [community]: https://thoughtbot.com/community?utm_source=github
170
+ [hire]: https://thoughtbot.com/hire-us?utm_source=github
@@ -1,23 +1,25 @@
1
- # -*- encoding: utf-8 -*-
2
1
  lib = File.expand_path("../lib", __FILE__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
3
  require "climate_control/version"
5
4
 
6
5
  Gem::Specification.new do |gem|
7
- gem.name = "climate_control"
8
- gem.version = ClimateControl::VERSION
9
- gem.authors = ["Joshua Clayton"]
10
- gem.email = ["joshua.clayton@gmail.com"]
11
- gem.description = %q{Modify your ENV}
12
- gem.summary = %q{Modify your ENV easily with ClimateControl}
13
- gem.homepage = "https://github.com/thoughtbot/climate_control"
14
- gem.license = "MIT"
6
+ gem.name = "climate_control"
7
+ gem.version = ClimateControl::VERSION
8
+ gem.authors = ["Joshua Clayton", "Dorian Marié"]
9
+ gem.email = ["joshua.clayton@gmail.com", "dorian@dorianmarie.fr"]
10
+ gem.description = "Modify your ENV"
11
+ gem.summary = "Modify your ENV easily with ClimateControl"
12
+ gem.homepage = "https://github.com/thoughtbot/climate_control"
13
+ gem.license = "MIT"
15
14
 
16
- gem.files = `git ls-files`.split($/)
17
- gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
17
  gem.require_paths = ["lib"]
19
18
 
20
- gem.add_development_dependency "rspec", "~> 3.1.0"
21
- gem.add_development_dependency "rake", "~> 10.3.2"
22
- gem.add_development_dependency "simplecov", "~> 0.9.1"
19
+ gem.required_ruby_version = ">= 2.5.0"
20
+
21
+ gem.add_development_dependency "rspec"
22
+ gem.add_development_dependency "rake"
23
+ gem.add_development_dependency "simplecov"
24
+ gem.add_development_dependency "standard"
23
25
  end
@@ -1,3 +1,3 @@
1
1
  module ClimateControl
2
- VERSION = "0.2.0".freeze
2
+ VERSION = "1.2.0".freeze
3
3
  end
@@ -1,16 +1,72 @@
1
- require "climate_control/environment"
2
1
  require "climate_control/errors"
3
- require "climate_control/modifier"
4
2
  require "climate_control/version"
3
+ require "monitor"
5
4
 
6
5
  module ClimateControl
7
- @@env = ClimateControl::Environment.new
6
+ extend self
7
+ extend Gem::Deprecate
8
8
 
9
- def self.modify(environment_overrides, &block)
10
- Modifier.new(env, environment_overrides, &block).process
9
+ SEMAPHORE = Monitor.new
10
+ private_constant :SEMAPHORE
11
+
12
+ def modify(environment_overrides = {}, &block)
13
+ environment_overrides = environment_overrides.transform_keys(&:to_s)
14
+
15
+ SEMAPHORE.synchronize do
16
+ previous = ENV.to_hash
17
+
18
+ begin
19
+ copy environment_overrides
20
+ ensure
21
+ middle = ENV.to_hash
22
+ end
23
+
24
+ block.call
25
+ ensure
26
+ after = ENV
27
+ (previous.keys | middle.keys | after.keys).each do |key|
28
+ if previous[key] != after[key] && middle[key] == after[key]
29
+ ENV[key] = previous[key]
30
+ end
31
+ end
32
+ end
11
33
  end
12
34
 
13
- def self.env
14
- @@env
35
+ def unsafe_modify(environment_overrides = {}, &block)
36
+ environment_overrides = environment_overrides.transform_keys(&:to_s)
37
+
38
+ previous = ENV.to_hash
39
+
40
+ begin
41
+ copy environment_overrides
42
+ ensure
43
+ middle = ENV.to_hash
44
+ end
45
+
46
+ block.call
47
+ ensure
48
+ after = ENV
49
+ (previous.keys | middle.keys | after.keys).each do |key|
50
+ if previous[key] != after[key] && middle[key] == after[key]
51
+ ENV[key] = previous[key]
52
+ end
53
+ end
54
+ end
55
+
56
+ def env
57
+ ENV
58
+ end
59
+
60
+ deprecate :env, "ENV", 2022, 10
61
+
62
+ private
63
+
64
+ def copy(overrides)
65
+ overrides.each do |key, value|
66
+ ENV[key] = value
67
+ rescue TypeError => e
68
+ raise UnassignableValueError,
69
+ "attempted to assign #{value} to #{key} but failed (#{e.message})"
70
+ end
15
71
  end
16
72
  end
@@ -1,9 +1,11 @@
1
1
  require "spec_helper"
2
2
 
3
+ Thing = Class.new
4
+
3
5
  describe "Climate control" do
4
6
  it "allows modification of the environment" do
5
7
  block_run = false
6
- ClimateControl.modify FOO: "bar" do
8
+ with_modified_env FOO: "bar" do
7
9
  expect(ENV["FOO"]).to eq "bar"
8
10
  block_run = true
9
11
  end
@@ -87,22 +89,22 @@ describe "Climate control" do
87
89
  # 0.25s passes
88
90
  # [other_thread] thread resolves, FOO is removed, BAZ is copied back to ENV
89
91
 
90
- thread_removing_env = Thread.new do
92
+ thread_removing_env = Thread.new {
91
93
  with_modified_env BAZ: "buzz" do
92
94
  sleep 0.5
93
95
  end
94
96
 
95
97
  expect(ENV["BAZ"]).to be_nil
96
- end
98
+ }
97
99
 
98
- other_thread = Thread.new do
100
+ other_thread = Thread.new {
99
101
  sleep 0.25
100
102
  with_modified_env FOO: "bar" do
101
103
  sleep 0.5
102
104
  end
103
105
 
104
106
  expect(ENV["FOO"]).to be_nil
105
- end
107
+ }
106
108
 
107
109
  thread_removing_env.join
108
110
  other_thread.join
@@ -111,12 +113,45 @@ describe "Climate control" do
111
113
  expect(ENV["BAZ"]).to be_nil
112
114
  end
113
115
 
116
+ it "handles threads accessing the same key wrapped in a block" do
117
+ first_thread = Thread.new {
118
+ with_modified_env do
119
+ with_modified_env CONFLICTING_KEY: "1" do
120
+ sleep 0.5
121
+ expect(ENV["CONFLICTING_KEY"]).to eq("1")
122
+ end
123
+
124
+ expect(ENV["CONFLICTING_KEY"]).to be_nil
125
+ end
126
+ }
127
+
128
+ second_thread = Thread.new {
129
+ with_modified_env do
130
+ sleep 0.25
131
+ expect(ENV["CONFLICTING_KEY"]).to be_nil
132
+
133
+ with_modified_env CONFLICTING_KEY: "2" do
134
+ expect(ENV["CONFLICTING_KEY"]).to eq("2")
135
+ sleep 0.5
136
+ expect(ENV["CONFLICTING_KEY"]).to eq("2")
137
+ end
138
+
139
+ expect(ENV["CONFLICTING_KEY"]).to be_nil
140
+ end
141
+ }
142
+
143
+ first_thread.join
144
+ second_thread.join
145
+
146
+ expect(ENV["CONFLICTING_KEY"]).to be_nil
147
+ end
148
+
114
149
  it "is re-entrant" do
115
- ret = with_modified_env(FOO: "foo") do
150
+ ret = with_modified_env(FOO: "foo") {
116
151
  with_modified_env(BAR: "bar") do
117
152
  "bar"
118
153
  end
119
- end
154
+ }
120
155
 
121
156
  expect(ret).to eq("bar")
122
157
 
@@ -125,15 +160,39 @@ describe "Climate control" do
125
160
  end
126
161
 
127
162
  it "raises when the value cannot be assigned properly" do
128
- Thing = Class.new
129
163
  message = generate_type_error_for_object(Thing.new)
130
164
 
131
- expect do
165
+ expect {
132
166
  with_modified_env(FOO: Thing.new)
133
- end.to raise_error ClimateControl::UnassignableValueError, /attempted to assign .*Thing.* to FOO but failed \(#{message}\)$/
167
+ }.to raise_error ClimateControl::UnassignableValueError, /attempted to assign .*Thing.* to FOO but failed \(#{message}\)$/
168
+ end
169
+
170
+ it "restores the ENV even when an error was raised when assigning values" do
171
+ ENV["KEY_TO_OVERRIDE"] = "initial_value_1"
172
+ ENV["KEY_THAT_WILL_ERROR_OUT"] = "initial_value_2"
173
+
174
+ expect {
175
+ with_modified_env(
176
+ KEY_TO_OVERRIDE: "overwriten_value_1",
177
+ KEY_THAT_WILL_ERROR_OUT: :value_that_will_error_out
178
+ ) {}
179
+ }.to raise_error ClimateControl::UnassignableValueError
180
+
181
+ expect(ENV["KEY_TO_OVERRIDE"]).to eq("initial_value_1")
182
+ expect(ENV["KEY_THAT_WILL_ERROR_OUT"]).to eq("initial_value_2")
183
+ end
184
+
185
+ it "doesn't block on nested modify calls" do
186
+ with_modified_env(SMS_DEFAULT_COUNTRY_CODE: nil) do
187
+ with_modified_env(SMS_DEFAULT_COUNTRY_CODE: "++56") do
188
+ expect(ENV.fetch("SMS_DEFAULT_COUNTRY_CODE", "++41")).to eq("++56")
189
+ end
190
+
191
+ expect(ENV.fetch("SMS_DEFAULT_COUNTRY_CODE", "++41")).to eq("++41")
192
+ end
134
193
  end
135
194
 
136
- def with_modified_env(options, &block)
195
+ def with_modified_env(options = {}, &block)
137
196
  ClimateControl.modify(options, &block)
138
197
  end
139
198
 
@@ -0,0 +1,186 @@
1
+ require "spec_helper"
2
+
3
+ describe "ClimateControl#unsafe_modify" do
4
+ it "allows modification of the environment" do
5
+ block_run = false
6
+ with_modified_env FOO: "bar" do
7
+ expect(ENV["FOO"]).to eq "bar"
8
+ block_run = true
9
+ end
10
+
11
+ expect(ENV["FOO"]).to be_nil
12
+ expect(block_run).to be true
13
+ end
14
+
15
+ it "modifies the environment" do
16
+ with_modified_env VARIABLE_1: "bar", VARIABLE_2: "qux" do
17
+ expect(ENV["VARIABLE_1"]).to eq "bar"
18
+ expect(ENV["VARIABLE_2"]).to eq "qux"
19
+ end
20
+
21
+ expect(ENV["VARIABLE_1"]).to be_nil
22
+ expect(ENV["VARIABLE_2"]).to be_nil
23
+ end
24
+
25
+ it "allows for environment variables to be assigned within the block" do
26
+ with_modified_env VARIABLE_1: "modified" do
27
+ ENV["ASSIGNED_IN_BLOCK"] = "assigned"
28
+ end
29
+
30
+ expect(ENV["ASSIGNED_IN_BLOCK"]).to eq "assigned"
31
+ end
32
+
33
+ it "reassigns previously set environment variables" do
34
+ ENV["VARIABLE_ASSIGNED_BEFORE_MODIFYING_ENV"] = "original"
35
+ expect(ENV["VARIABLE_ASSIGNED_BEFORE_MODIFYING_ENV"]).to eq "original"
36
+
37
+ with_modified_env VARIABLE_ASSIGNED_BEFORE_MODIFYING_ENV: "overridden" do
38
+ expect(ENV["VARIABLE_ASSIGNED_BEFORE_MODIFYING_ENV"]).to eq "overridden"
39
+ end
40
+
41
+ expect(ENV["VARIABLE_ASSIGNED_BEFORE_MODIFYING_ENV"]).to eq "original"
42
+ end
43
+
44
+ it "persists the change when overriding the variable in the block" do
45
+ with_modified_env VARIABLE_MODIFIED_AND_THEN_ASSIGNED: "modified" do
46
+ ENV["VARIABLE_MODIFIED_AND_THEN_ASSIGNED"] = "assigned value"
47
+ end
48
+
49
+ expect(ENV["VARIABLE_MODIFIED_AND_THEN_ASSIGNED"]).to eq "assigned value"
50
+ end
51
+
52
+ it "resets environment variables even if the block raises" do
53
+ expect {
54
+ with_modified_env FOO: "bar" do
55
+ raise "broken"
56
+ end
57
+ }.to raise_error("broken")
58
+
59
+ expect(ENV["FOO"]).to be_nil
60
+ end
61
+
62
+ it "preserves environment variables set within the block" do
63
+ ENV["CHANGED"] = "old value"
64
+
65
+ with_modified_env IRRELEVANT: "ignored value" do
66
+ ENV["CHANGED"] = "new value"
67
+ end
68
+
69
+ expect(ENV["CHANGED"]).to eq "new value"
70
+ end
71
+
72
+ it "returns the value of the block" do
73
+ value = with_modified_env VARIABLE_1: "bar" do
74
+ "value inside block"
75
+ end
76
+
77
+ expect(value).to eq "value inside block"
78
+ end
79
+
80
+ it "handles threads correctly" do
81
+ # failure path without mutex
82
+ # [thread_removing_env] BAZ is assigned
83
+ # 0.25s passes
84
+ # [other_thread] FOO is assigned and ENV is copied (which includes BAZ)
85
+ # 0.25s passes
86
+ # [thread_removing_env] thread resolves and BAZ is removed from env; other_thread still retains knowledge of BAZ
87
+ # 0.25s passes
88
+ # [other_thread] thread resolves, FOO is removed, BAZ is copied back to ENV
89
+
90
+ thread_removing_env = Thread.new {
91
+ with_modified_env BAZ: "buzz" do
92
+ sleep 0.5
93
+ end
94
+
95
+ expect(ENV["BAZ"]).to be_nil
96
+ }
97
+
98
+ other_thread = Thread.new {
99
+ sleep 0.25
100
+ with_modified_env FOO: "bar" do
101
+ sleep 0.5
102
+ end
103
+
104
+ expect(ENV["FOO"]).to be_nil
105
+ }
106
+
107
+ thread_removing_env.join
108
+ other_thread.join
109
+
110
+ expect(ENV["FOO"]).to be_nil
111
+ expect(ENV["BAZ"]).to be_nil
112
+ end
113
+
114
+ it "is re-entrant" do
115
+ ret = with_modified_env(FOO: "foo") {
116
+ with_modified_env(BAR: "bar") do
117
+ "bar"
118
+ end
119
+ }
120
+
121
+ expect(ret).to eq("bar")
122
+
123
+ expect(ENV["FOO"]).to be_nil
124
+ expect(ENV["BAR"]).to be_nil
125
+ end
126
+
127
+ it "raises when the value cannot be assigned properly" do
128
+ message = generate_type_error_for_object(Thing.new)
129
+
130
+ expect {
131
+ with_modified_env(FOO: Thing.new)
132
+ }.to raise_error ClimateControl::UnassignableValueError, /attempted to assign .*Thing.* to FOO but failed \(#{message}\)$/
133
+ end
134
+
135
+ it "restores the ENV even when an error was raised when assigning values" do
136
+ ENV["KEY_TO_OVERRIDE"] = "initial_value_1"
137
+ ENV["KEY_THAT_WILL_ERROR_OUT"] = "initial_value_2"
138
+
139
+ expect {
140
+ with_modified_env(
141
+ KEY_TO_OVERRIDE: "overwriten_value_1",
142
+ KEY_THAT_WILL_ERROR_OUT: :value_that_will_error_out
143
+ ) {}
144
+ }.to raise_error ClimateControl::UnassignableValueError
145
+
146
+ expect(ENV["KEY_TO_OVERRIDE"]).to eq("initial_value_1")
147
+ expect(ENV["KEY_THAT_WILL_ERROR_OUT"]).to eq("initial_value_2")
148
+ end
149
+
150
+ it "doesn't block on nested modify calls" do
151
+ with_modified_env(SMS_DEFAULT_COUNTRY_CODE: nil) do
152
+ with_modified_env(SMS_DEFAULT_COUNTRY_CODE: "++56") do
153
+ expect(ENV.fetch("SMS_DEFAULT_COUNTRY_CODE", "++41")).to eq("++56")
154
+ end
155
+
156
+ expect(ENV.fetch("SMS_DEFAULT_COUNTRY_CODE", "++41")).to eq("++41")
157
+ end
158
+ end
159
+
160
+ def with_modified_env(options = {}, &block)
161
+ ClimateControl.unsafe_modify(options, &block)
162
+ end
163
+
164
+ def generate_type_error_for_object(object)
165
+ message = nil
166
+
167
+ begin
168
+ "1" + object
169
+ rescue TypeError => e
170
+ message = e.message
171
+ end
172
+
173
+ message
174
+ end
175
+
176
+ around do |example|
177
+ old_env = ENV.to_hash
178
+
179
+ example.run
180
+
181
+ ENV.clear
182
+ old_env.each do |key, value|
183
+ ENV[key] = value
184
+ end
185
+ end
186
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  begin
2
- require 'simplecov'
2
+ require "simplecov"
3
3
  SimpleCov.start
4
4
  rescue LoadError
5
5
  warn "warning: simplecov gem not found; skipping coverage"
metadata CHANGED
@@ -1,84 +1,101 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: climate_control
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Clayton
8
- autorequire:
8
+ - Dorian Marié
9
+ autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2017-05-12 00:00:00.000000000 Z
12
+ date: 2022-07-15 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: rspec
15
16
  requirement: !ruby/object:Gem::Requirement
16
17
  requirements:
17
- - - "~>"
18
+ - - ">="
18
19
  - !ruby/object:Gem::Version
19
- version: 3.1.0
20
+ version: '0'
20
21
  type: :development
21
22
  prerelease: false
22
23
  version_requirements: !ruby/object:Gem::Requirement
23
24
  requirements:
24
- - - "~>"
25
+ - - ">="
25
26
  - !ruby/object:Gem::Version
26
- version: 3.1.0
27
+ version: '0'
27
28
  - !ruby/object:Gem::Dependency
28
29
  name: rake
29
30
  requirement: !ruby/object:Gem::Requirement
30
31
  requirements:
31
- - - "~>"
32
+ - - ">="
32
33
  - !ruby/object:Gem::Version
33
- version: 10.3.2
34
+ version: '0'
34
35
  type: :development
35
36
  prerelease: false
36
37
  version_requirements: !ruby/object:Gem::Requirement
37
38
  requirements:
38
- - - "~>"
39
+ - - ">="
39
40
  - !ruby/object:Gem::Version
40
- version: 10.3.2
41
+ version: '0'
41
42
  - !ruby/object:Gem::Dependency
42
43
  name: simplecov
43
44
  requirement: !ruby/object:Gem::Requirement
44
45
  requirements:
45
- - - "~>"
46
+ - - ">="
46
47
  - !ruby/object:Gem::Version
47
- version: 0.9.1
48
+ version: '0'
48
49
  type: :development
49
50
  prerelease: false
50
51
  version_requirements: !ruby/object:Gem::Requirement
51
52
  requirements:
52
- - - "~>"
53
+ - - ">="
53
54
  - !ruby/object:Gem::Version
54
- version: 0.9.1
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: standard
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
55
70
  description: Modify your ENV
56
71
  email:
57
72
  - joshua.clayton@gmail.com
73
+ - dorian@dorianmarie.fr
58
74
  executables: []
59
75
  extensions: []
60
76
  extra_rdoc_files: []
61
77
  files:
78
+ - ".github/workflows/ci.yml"
62
79
  - ".gitignore"
63
- - ".travis.yml"
80
+ - CHANGELOG.md
81
+ - CODEOWNERS
82
+ - CODE_OF_CONDUCT.md
64
83
  - Gemfile
65
- - LICENSE.txt
66
- - NEWS
84
+ - LICENSE
67
85
  - README.md
68
86
  - Rakefile
69
87
  - climate_control.gemspec
70
88
  - lib/climate_control.rb
71
- - lib/climate_control/environment.rb
72
89
  - lib/climate_control/errors.rb
73
- - lib/climate_control/modifier.rb
74
90
  - lib/climate_control/version.rb
75
91
  - spec/acceptance/climate_control_spec.rb
92
+ - spec/acceptance/unsafe_modify_spec.rb
76
93
  - spec/spec_helper.rb
77
94
  homepage: https://github.com/thoughtbot/climate_control
78
95
  licenses:
79
96
  - MIT
80
97
  metadata: {}
81
- post_install_message:
98
+ post_install_message:
82
99
  rdoc_options: []
83
100
  require_paths:
84
101
  - lib
@@ -86,18 +103,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
86
103
  requirements:
87
104
  - - ">="
88
105
  - !ruby/object:Gem::Version
89
- version: '0'
106
+ version: 2.5.0
90
107
  required_rubygems_version: !ruby/object:Gem::Requirement
91
108
  requirements:
92
109
  - - ">="
93
110
  - !ruby/object:Gem::Version
94
111
  version: '0'
95
112
  requirements: []
96
- rubyforge_project:
97
- rubygems_version: 2.5.2
98
- signing_key:
113
+ rubygems_version: 3.3.13
114
+ signing_key:
99
115
  specification_version: 4
100
116
  summary: Modify your ENV easily with ClimateControl
101
117
  test_files:
102
118
  - spec/acceptance/climate_control_spec.rb
119
+ - spec/acceptance/unsafe_modify_spec.rb
103
120
  - spec/spec_helper.rb
data/.travis.yml DELETED
@@ -1,11 +0,0 @@
1
- rvm:
2
- - 2.2.6
3
- - 2.3.3
4
- - 2.4.0
5
- before_install:
6
- - gem update --system
7
- - gem update bundler
8
- install: "bundle install"
9
- branches:
10
- only:
11
- - master
data/NEWS DELETED
@@ -1,13 +0,0 @@
1
- 0.2.0 (May 12, 2017)
2
- Allow nested environment changes in the same thread
3
-
4
- 0.1.0 (January 7, 2017)
5
- Remove ActiveSupport dependency
6
-
7
- 0.0.4 (January 6, 2017)
8
- Improved thread safety
9
- Handle TypeErrors during assignment
10
- Improve documentation
11
-
12
- 0.0.1 (November 28, 2012)
13
- Initial release
@@ -1,36 +0,0 @@
1
- require "thread"
2
- require "forwardable"
3
-
4
- module ClimateControl
5
- class Environment
6
- extend Forwardable
7
-
8
- def initialize
9
- @semaphore = Mutex.new
10
- @owner = nil
11
- end
12
-
13
- def_delegators :env, :[]=, :to_hash, :[], :delete
14
-
15
- def synchronize
16
- if @owner == Thread.current
17
- return yield if block_given?
18
- end
19
-
20
- @semaphore.synchronize do
21
- begin
22
- @owner = Thread.current
23
- yield if block_given?
24
- ensure
25
- @owner = nil
26
- end
27
- end
28
- end
29
-
30
- private
31
-
32
- def env
33
- ENV
34
- end
35
- end
36
- end
@@ -1,96 +0,0 @@
1
- module ClimateControl
2
- class Modifier
3
- def initialize(env, environment_overrides = {}, &block)
4
- @environment_overrides = stringify_keys(environment_overrides)
5
- @block = block
6
- @env = env
7
- end
8
-
9
- def process
10
- @env.synchronize do
11
- begin
12
- prepare_environment_for_block
13
- run_block
14
- ensure
15
- cache_environment_after_block
16
- delete_keys_that_do_not_belong
17
- revert_changed_keys
18
- end
19
- end
20
- end
21
-
22
- private
23
-
24
- def prepare_environment_for_block
25
- @original_env = clone_environment
26
- copy_overrides_to_environment
27
- @env_with_overrides_before_block = clone_environment
28
- end
29
-
30
- def run_block
31
- @block.call
32
- end
33
-
34
- def copy_overrides_to_environment
35
- @environment_overrides.each do |key, value|
36
- begin
37
- @env[key] = value
38
- rescue TypeError => e
39
- raise UnassignableValueError,
40
- "attempted to assign #{value} to #{key} but failed (#{e.message})"
41
- end
42
- end
43
- end
44
-
45
- def keys_to_remove
46
- @environment_overrides.keys
47
- end
48
-
49
- def keys_changed_by_block
50
- @keys_changed_by_block ||= OverlappingKeysWithChangedValues.new(@env_with_overrides_before_block, @env_after_block).keys
51
- end
52
-
53
- def cache_environment_after_block
54
- @env_after_block = clone_environment
55
- end
56
-
57
- def delete_keys_that_do_not_belong
58
- (keys_to_remove - keys_changed_by_block).each {|key| @env.delete(key) }
59
- end
60
-
61
- def revert_changed_keys
62
- (@original_env.keys - keys_changed_by_block).each do |key|
63
- @env[key] = @original_env[key]
64
- end
65
- end
66
-
67
- def clone_environment
68
- @env.to_hash
69
- end
70
-
71
- def stringify_keys(env)
72
- env.each_with_object({}) do |(key, value), hash|
73
- hash[key.to_s] = value
74
- end
75
- end
76
-
77
- class OverlappingKeysWithChangedValues
78
- def initialize(hash_1, hash_2)
79
- @hash_1 = hash_1 || {}
80
- @hash_2 = hash_2
81
- end
82
-
83
- def keys
84
- overlapping_keys.select do |overlapping_key|
85
- @hash_1[overlapping_key] != @hash_2[overlapping_key]
86
- end
87
- end
88
-
89
- private
90
-
91
- def overlapping_keys
92
- @hash_2.keys & @hash_1.keys
93
- end
94
- end
95
- end
96
- end