climate_control 1.0.0 → 1.1.1

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
2
  SHA256:
3
- metadata.gz: 65eb94d5a3f27af965178320b8b470c81b36cc646c55eda3b6471db78fca49a4
4
- data.tar.gz: ba3abd5ba34a63c23f77195a7f9d4c8e5853f313812dc95b5d9ed37032f964c8
3
+ metadata.gz: 1052215413e209a2ce0531a9b293d56f51df36db70688af69ae15e70571a043d
4
+ data.tar.gz: ed855bfdddc57fc46532edc296d9a8de6a2a3653f5b148ab10849433aeb61026
5
5
  SHA512:
6
- metadata.gz: 0a456cc8d8f3fb15cce8bf3401fbbf954b6ad2e7427a20a02fb35abf1da7852833ee8da7862ca4dfa2f98ef9ed62a29841c18d6ab5f40a1d3192073cd73bc621
7
- data.tar.gz: 21879ffe4cc583c24fcb7e61fe1617f5126c5f606b4327cc6384cc81dfbff061fff6262c343054dee166f75c109769981354918c2e0481da4412dbf2cf211207
6
+ metadata.gz: e292d7055db48aaa3cf664f447c6826d3ccf59cc1142dc71a89e8b8e97dbac26d553ebd2912a1f8b2a15b3f75b71de5f3345244cc0cb2f4dd8970f47b73b12c1
7
+ data.tar.gz: 96f9e0f2e8193c29d708f94c1cdced3d7a1b305ae0cb781729895b838be70233a0082f767f840a5d943e5b206cc55539606de156e4b301bab787e29ab45507b4
@@ -6,7 +6,7 @@ jobs:
6
6
  runs-on: ubuntu-latest
7
7
  strategy:
8
8
  matrix:
9
- ruby-version: [3.0, 2.7, 2.6, 2.5]
9
+ ruby-version: [3.1, '3.0', 2.7, 2.6, 2.5]
10
10
  steps:
11
11
  - uses: actions/checkout@v2
12
12
  - name: Set up Ruby ${{ matrix.ruby-version }}
data/CHANGELOG.md ADDED
@@ -0,0 +1,45 @@
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
+ ## Unreleased
9
+
10
+ ## 1.1.0 / 2022-05-26
11
+
12
+ - Refactor to use `Monitor` instead of `Mutex`
13
+ - Add documentation about thread-safety
14
+ - Allow ClimateControl.modify to be called without environment variables
15
+ - Add test for concurrent access needed to be inside block
16
+ - Relax development dependencies
17
+
18
+ ## 1.0.1 / 2021-05-26
19
+
20
+ - Require minimum Ruby version of 2.5.0
21
+
22
+ # 1.0.0 / 2021-03-06
23
+
24
+ - Commit to supporting latest patch versions of Ruby 2.5+
25
+ - Improve documentation
26
+ - Format code with StandardRB
27
+ - Bump gem dependencies
28
+
29
+ # 0.2.0 / 2017-05-12
30
+
31
+ - Allow nested environment changes in the same thread
32
+
33
+ # 0.1.0 / 2017-01-07
34
+
35
+ - Remove ActiveSupport dependency
36
+
37
+ # 0.0.4 / 2017-01-06
38
+
39
+ - Improved thread safety
40
+ - Handle TypeErrors during assignment
41
+ - Improve documentation
42
+
43
+ # 0.0.1 / 2012-11-28
44
+
45
+ - Initial release
data/CODEOWNERS ADDED
@@ -0,0 +1 @@
1
+ * @dorianmariefr
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
@@ -48,7 +50,7 @@ end
48
50
  To use with RSpec, you could define this in your spec:
49
51
 
50
52
  ```ruby
51
- def with_modified_env(options, &block)
53
+ def with_modified_env(options = {}, &block)
52
54
  ClimateControl.modify(options, &block)
53
55
  end
54
56
  ```
@@ -105,6 +107,39 @@ manner becomes more difficult:
105
107
  Climate Control modifies environment variables only within the context of the
106
108
  block, ensuring values are managed properly and consistently.
107
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
+
108
143
  ## Contributing
109
144
 
110
145
  1. Fork it
@@ -118,3 +153,18 @@ This project uses [StandardRB](https://github.com/testdouble/standard) to ensure
118
153
  ## License
119
154
 
120
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
@@ -5,8 +5,8 @@ require "climate_control/version"
5
5
  Gem::Specification.new do |gem|
6
6
  gem.name = "climate_control"
7
7
  gem.version = ClimateControl::VERSION
8
- gem.authors = ["Joshua Clayton"]
9
- gem.email = ["joshua.clayton@gmail.com"]
8
+ gem.authors = ["Joshua Clayton", "Dorian Marié"]
9
+ gem.email = ["joshua.clayton@gmail.com", "dorian@dorianmarie.fr"]
10
10
  gem.description = "Modify your ENV"
11
11
  gem.summary = "Modify your ENV easily with ClimateControl"
12
12
  gem.homepage = "https://github.com/thoughtbot/climate_control"
@@ -16,8 +16,10 @@ Gem::Specification.new do |gem|
16
16
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
17
  gem.require_paths = ["lib"]
18
18
 
19
- gem.add_development_dependency "rspec", "~> 3.10.0"
20
- gem.add_development_dependency "rake", "~> 12.3.3"
21
- gem.add_development_dependency "simplecov", "~> 0.9.1"
22
- gem.add_development_dependency "standard", "~> 1.0.0"
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 = "1.0.0".freeze
2
+ VERSION = "1.1.1".freeze
3
3
  end
@@ -1,16 +1,48 @@
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
8
7
 
9
- def self.modify(environment_overrides, &block)
10
- Modifier.new(env, environment_overrides, &block).process
8
+ SEMAPHORE = Monitor.new
9
+ private_constant :SEMAPHORE
10
+
11
+ def modify(environment_overrides = {}, &block)
12
+ environment_overrides = environment_overrides.transform_keys(&:to_s)
13
+
14
+ SEMAPHORE.synchronize do
15
+ previous = ENV.to_hash
16
+
17
+ begin
18
+ copy environment_overrides
19
+ ensure
20
+ middle = ENV.to_hash
21
+ end
22
+
23
+ block.call
24
+ ensure
25
+ after = ENV
26
+ (previous.keys | middle.keys | after.keys).each do |key|
27
+ if previous[key] != after[key] && middle[key] == after[key]
28
+ ENV[key] = previous[key]
29
+ end
30
+ end
31
+ end
11
32
  end
12
33
 
13
- def self.env
14
- @@env
34
+ def env
35
+ ENV
36
+ end
37
+
38
+ private
39
+
40
+ def copy(overrides)
41
+ overrides.each do |key, value|
42
+ ENV[key] = value
43
+ rescue TypeError => e
44
+ raise UnassignableValueError,
45
+ "attempted to assign #{value} to #{key} but failed (#{e.message})"
46
+ end
15
47
  end
16
48
  end
@@ -113,6 +113,39 @@ describe "Climate control" do
113
113
  expect(ENV["BAZ"]).to be_nil
114
114
  end
115
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
+
116
149
  it "is re-entrant" do
117
150
  ret = with_modified_env(FOO: "foo") {
118
151
  with_modified_env(BAR: "bar") do
@@ -134,7 +167,22 @@ describe "Climate control" do
134
167
  }.to raise_error ClimateControl::UnassignableValueError, /attempted to assign .*Thing.* to FOO but failed \(#{message}\)$/
135
168
  end
136
169
 
137
- def with_modified_env(options, &block)
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
+ def with_modified_env(options = {}, &block)
138
186
  ClimateControl.modify(options, &block)
139
187
  end
140
188
 
metadata CHANGED
@@ -1,91 +1,92 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: climate_control
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.1
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: 2021-03-07 00:00:00.000000000 Z
12
+ date: 2022-05-28 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.10.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.10.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: 12.3.3
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: 12.3.3
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'
55
56
  - !ruby/object:Gem::Dependency
56
57
  name: standard
57
58
  requirement: !ruby/object:Gem::Requirement
58
59
  requirements:
59
- - - "~>"
60
+ - - ">="
60
61
  - !ruby/object:Gem::Version
61
- version: 1.0.0
62
+ version: '0'
62
63
  type: :development
63
64
  prerelease: false
64
65
  version_requirements: !ruby/object:Gem::Requirement
65
66
  requirements:
66
- - - "~>"
67
+ - - ">="
67
68
  - !ruby/object:Gem::Version
68
- version: 1.0.0
69
+ version: '0'
69
70
  description: Modify your ENV
70
71
  email:
71
72
  - joshua.clayton@gmail.com
73
+ - dorian@dorianmarie.fr
72
74
  executables: []
73
75
  extensions: []
74
76
  extra_rdoc_files: []
75
77
  files:
76
78
  - ".github/workflows/ci.yml"
77
79
  - ".gitignore"
80
+ - CHANGELOG.md
81
+ - CODEOWNERS
78
82
  - CODE_OF_CONDUCT.md
79
83
  - Gemfile
80
84
  - LICENSE
81
- - NEWS
82
85
  - README.md
83
86
  - Rakefile
84
87
  - climate_control.gemspec
85
88
  - lib/climate_control.rb
86
- - lib/climate_control/environment.rb
87
89
  - lib/climate_control/errors.rb
88
- - lib/climate_control/modifier.rb
89
90
  - lib/climate_control/version.rb
90
91
  - spec/acceptance/climate_control_spec.rb
91
92
  - spec/spec_helper.rb
@@ -93,7 +94,7 @@ homepage: https://github.com/thoughtbot/climate_control
93
94
  licenses:
94
95
  - MIT
95
96
  metadata: {}
96
- post_install_message:
97
+ post_install_message:
97
98
  rdoc_options: []
98
99
  require_paths:
99
100
  - lib
@@ -101,15 +102,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
101
102
  requirements:
102
103
  - - ">="
103
104
  - !ruby/object:Gem::Version
104
- version: '0'
105
+ version: 2.5.0
105
106
  required_rubygems_version: !ruby/object:Gem::Requirement
106
107
  requirements:
107
108
  - - ">="
108
109
  - !ruby/object:Gem::Version
109
110
  version: '0'
110
111
  requirements: []
111
- rubygems_version: 3.1.4
112
- signing_key:
112
+ rubygems_version: 3.3.13
113
+ signing_key:
113
114
  specification_version: 4
114
115
  summary: Modify your ENV easily with ClimateControl
115
116
  test_files:
data/NEWS DELETED
@@ -1,19 +0,0 @@
1
- 1.0.0 (March 6, 2021)
2
- Commit to supporting latest patch versions of Ruby 2.5+
3
- Improve documentation
4
- Format code with StandardRB
5
- Bump gem dependencies
6
-
7
- 0.2.0 (May 12, 2017)
8
- Allow nested environment changes in the same thread
9
-
10
- 0.1.0 (January 7, 2017)
11
- Remove ActiveSupport dependency
12
-
13
- 0.0.4 (January 6, 2017)
14
- Improved thread safety
15
- Handle TypeErrors during assignment
16
- Improve documentation
17
-
18
- 0.0.1 (November 28, 2012)
19
- Initial release
@@ -1,33 +0,0 @@
1
- require "forwardable"
2
-
3
- module ClimateControl
4
- class Environment
5
- extend Forwardable
6
-
7
- def initialize
8
- @semaphore = Mutex.new
9
- @owner = nil
10
- end
11
-
12
- def_delegators :env, :[]=, :to_hash, :[], :delete
13
-
14
- def synchronize
15
- if @owner == Thread.current
16
- return yield if block_given?
17
- end
18
-
19
- @semaphore.synchronize do
20
- @owner = Thread.current
21
- yield if block_given?
22
- ensure
23
- @owner = nil
24
- end
25
- end
26
-
27
- private
28
-
29
- def env
30
- ENV
31
- end
32
- end
33
- end
@@ -1,92 +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
- prepare_environment_for_block
12
- run_block
13
- ensure
14
- cache_environment_after_block
15
- delete_keys_that_do_not_belong
16
- revert_changed_keys
17
- end
18
- end
19
-
20
- private
21
-
22
- def prepare_environment_for_block
23
- @original_env = clone_environment
24
- copy_overrides_to_environment
25
- @env_with_overrides_before_block = clone_environment
26
- end
27
-
28
- def run_block
29
- @block.call
30
- end
31
-
32
- def copy_overrides_to_environment
33
- @environment_overrides.each do |key, value|
34
- @env[key] = value
35
- rescue TypeError => e
36
- raise UnassignableValueError,
37
- "attempted to assign #{value} to #{key} but failed (#{e.message})"
38
- end
39
- end
40
-
41
- def keys_to_remove
42
- @environment_overrides.keys
43
- end
44
-
45
- def keys_changed_by_block
46
- @keys_changed_by_block ||= OverlappingKeysWithChangedValues.new(@env_with_overrides_before_block, @env_after_block).keys
47
- end
48
-
49
- def cache_environment_after_block
50
- @env_after_block = clone_environment
51
- end
52
-
53
- def delete_keys_that_do_not_belong
54
- (keys_to_remove - keys_changed_by_block).each { |key| @env.delete(key) }
55
- end
56
-
57
- def revert_changed_keys
58
- (@original_env.keys - keys_changed_by_block).each do |key|
59
- @env[key] = @original_env[key]
60
- end
61
- end
62
-
63
- def clone_environment
64
- @env.to_hash
65
- end
66
-
67
- def stringify_keys(env)
68
- env.each_with_object({}) do |(key, value), hash|
69
- hash[key.to_s] = value
70
- end
71
- end
72
-
73
- class OverlappingKeysWithChangedValues
74
- def initialize(hash_1, hash_2)
75
- @hash_1 = hash_1 || {}
76
- @hash_2 = hash_2
77
- end
78
-
79
- def keys
80
- overlapping_keys.select do |overlapping_key|
81
- @hash_1[overlapping_key] != @hash_2[overlapping_key]
82
- end
83
- end
84
-
85
- private
86
-
87
- def overlapping_keys
88
- @hash_2.keys & @hash_1.keys
89
- end
90
- end
91
- end
92
- end