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 +5 -5
- data/.github/workflows/ci.yml +23 -0
- data/CHANGELOG.md +53 -0
- data/CODEOWNERS +1 -0
- data/CODE_OF_CONDUCT.md +6 -0
- data/{LICENSE.txt → LICENSE} +1 -1
- data/README.md +71 -3
- data/climate_control.gemspec +16 -14
- data/lib/climate_control/version.rb +1 -1
- data/lib/climate_control.rb +63 -7
- data/spec/acceptance/climate_control_spec.rb +70 -11
- data/spec/acceptance/unsafe_modify_spec.rb +186 -0
- data/spec/spec_helper.rb +1 -1
- metadata +42 -25
- data/.travis.yml +0 -11
- data/NEWS +0 -13
- data/lib/climate_control/environment.rb +0 -36
- data/lib/climate_control/modifier.rb +0 -96
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: '00584062039c6b9161ff0e4a1b61ca51e4d8742225fc611180c4effcc43959c5'
|
4
|
+
data.tar.gz: 99135f45c57f58d87e17480896045c00eecebd632ed64eaf18bd1964ef7d454d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/CODE_OF_CONDUCT.md
ADDED
data/{LICENSE.txt → LICENSE}
RENAMED
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# Climate Control
|
2
2
|
|
3
|
+

|
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
|
-
|
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-
|
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
|
+

|
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
|
data/climate_control.gemspec
CHANGED
@@ -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
|
8
|
-
gem.version
|
9
|
-
gem.authors
|
10
|
-
gem.email
|
11
|
-
gem.description
|
12
|
-
gem.summary
|
13
|
-
gem.homepage
|
14
|
-
gem.license
|
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
|
17
|
-
gem.test_files
|
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.
|
21
|
-
|
22
|
-
gem.add_development_dependency "
|
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
|
data/lib/climate_control.rb
CHANGED
@@ -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
|
-
|
6
|
+
extend self
|
7
|
+
extend Gem::Deprecate
|
8
8
|
|
9
|
-
|
10
|
-
|
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
|
14
|
-
|
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
|
-
|
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
|
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
|
-
|
98
|
+
}
|
97
99
|
|
98
|
-
other_thread = Thread.new
|
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
|
-
|
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")
|
150
|
+
ret = with_modified_env(FOO: "foo") {
|
116
151
|
with_modified_env(BAR: "bar") do
|
117
152
|
"bar"
|
118
153
|
end
|
119
|
-
|
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
|
165
|
+
expect {
|
132
166
|
with_modified_env(FOO: Thing.new)
|
133
|
-
|
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
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:
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joshua Clayton
|
8
|
-
|
8
|
+
- Dorian Marié
|
9
|
+
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date:
|
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:
|
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:
|
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:
|
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:
|
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
|
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
|
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
|
-
-
|
80
|
+
- CHANGELOG.md
|
81
|
+
- CODEOWNERS
|
82
|
+
- CODE_OF_CONDUCT.md
|
64
83
|
- Gemfile
|
65
|
-
- LICENSE
|
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:
|
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
|
-
|
97
|
-
|
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
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
|