climate_control 0.2.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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