climate_control 1.0.1 → 1.1.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
2
  SHA256:
3
- metadata.gz: aac4a1d4fe272e5369855d9d80c8bfd764b752f27d1bdfad2b19a607d5f0a44e
4
- data.tar.gz: 8945986d5956f681810ef8a5f164323a3cd2a48e82368618c292f3c6fc7a6caa
3
+ metadata.gz: 919bb6d645214941bca1f5de4d30732891348bb76a29725e9db8ba00b6274dbc
4
+ data.tar.gz: 332e9222519c1c6a91d866783d4c3993c4372ffe29242f60759cf9948636d3ff
5
5
  SHA512:
6
- metadata.gz: c36a6b4ead29b23f93c95457e56f6bc3fd6c5cd596f66fe8243e736edcc5a11cfe9fd5fe62b1cf5fc8cd0c7f4fe72c051c0560338a6496e30c56b3cf095c92ec
7
- data.tar.gz: cbdf93ff0a149ef67129db0ac0f25838f02c7d2faf6dea87f8ce0acaf54848fb129f4d1b8cbf0ad40c50358cd2186e5a726b2fed5d82fbc9de2c765f4a9bf94d
6
+ metadata.gz: 1c5c9c97dcef22aa4923d284f196addeb1ddeb2f87c80785fd5d8a38e3f23ea7e66e11acbe265e225bf3663d36dbe500edc981d5baaf53cc872a19670dfa1878
7
+ data.tar.gz: c56fb5c40969ed979a485503765b33f7f4adc2d34830309502a2207dad363f4c9a0b04c5dda5cb4c56c001e80c317954a8d19b0b21fce53aa0fd84962d33c091
@@ -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
@@ -48,7 +48,7 @@ end
48
48
  To use with RSpec, you could define this in your spec:
49
49
 
50
50
  ```ruby
51
- def with_modified_env(options, &block)
51
+ def with_modified_env(options = {}, &block)
52
52
  ClimateControl.modify(options, &block)
53
53
  end
54
54
  ```
@@ -105,6 +105,39 @@ manner becomes more difficult:
105
105
  Climate Control modifies environment variables only within the context of the
106
106
  block, ensuring values are managed properly and consistently.
107
107
 
108
+ ## Thread-safety
109
+
110
+ When using threads, for instance when running tests concurrently in the same
111
+ process, you may need to wrap your code inside `ClimateControl.modify` blocks,
112
+ e.g.:
113
+
114
+ ```ruby
115
+ first_thread = Thread.new do
116
+ ClimateControl.modify(SECRET: "1") do
117
+ p ENV["SECRET"] # => "1"
118
+ sleep 2
119
+ p ENV["SECRET"] # => "1"
120
+ end
121
+ end
122
+
123
+ second_thread = Thread.new do
124
+ ClimateControl.modify({}) do
125
+ sleep 1
126
+ p ENV["SECRET"] # => nil
127
+ sleep 1
128
+ p ENV["SECRET"] # => nil
129
+ end
130
+ end
131
+
132
+ first_thread.join
133
+ second_thread.join
134
+ ```
135
+
136
+ > 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).
137
+ >
138
+ > <cite><a href="https://github.com/thoughtbot/climate_control/issues/32#issuecomment-800713686">Josh Clayton</a></cite>
139
+
140
+
108
141
  ## Contributing
109
142
 
110
143
  1. Fork it
@@ -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"
@@ -18,8 +18,8 @@ Gem::Specification.new do |gem|
18
18
 
19
19
  gem.required_ruby_version = ">= 2.5.0"
20
20
 
21
- gem.add_development_dependency "rspec", "~> 3.10.0"
22
- gem.add_development_dependency "rake", "~> 12.3.3"
23
- gem.add_development_dependency "simplecov", "~> 0.9.1"
24
- gem.add_development_dependency "standard", "~> 1.0.0"
21
+ gem.add_development_dependency "rspec"
22
+ gem.add_development_dependency "rake"
23
+ gem.add_development_dependency "simplecov"
24
+ gem.add_development_dependency "standard"
25
25
  end
@@ -1,3 +1,3 @@
1
1
  module ClimateControl
2
- VERSION = "1.0.1".freeze
2
+ VERSION = "1.1.0".freeze
3
3
  end
@@ -1,16 +1,44 @@
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
+ middle = {}
17
+ copy environment_overrides
18
+ middle = ENV.to_hash
19
+ block.call
20
+ ensure
21
+ after = ENV
22
+ (previous.keys | middle.keys | after.keys).each do |key|
23
+ if previous[key] != after[key] && middle[key] == after[key]
24
+ ENV[key] = previous[key]
25
+ end
26
+ end
27
+ end
11
28
  end
12
29
 
13
- def self.env
14
- @@env
30
+ def env
31
+ ENV
32
+ end
33
+
34
+ private
35
+
36
+ def copy(overrides)
37
+ overrides.each do |key, value|
38
+ ENV[key] = value
39
+ rescue TypeError => e
40
+ raise UnassignableValueError,
41
+ "attempted to assign #{value} to #{key} but failed (#{e.message})"
42
+ end
15
43
  end
16
44
  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,7 @@ 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
+ def with_modified_env(options = {}, &block)
138
171
  ClimateControl.modify(options, &block)
139
172
  end
140
173
 
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.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Clayton
8
+ - Dorian Marié
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2021-05-27 00:00:00.000000000 Z
12
+ date: 2022-05-26 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
@@ -108,7 +109,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
108
109
  - !ruby/object:Gem::Version
109
110
  version: '0'
110
111
  requirements: []
111
- rubygems_version: 3.2.3
112
+ rubygems_version: 3.3.13
112
113
  signing_key:
113
114
  specification_version: 4
114
115
  summary: Modify your ENV easily with ClimateControl
data/NEWS DELETED
@@ -1,22 +0,0 @@
1
- 1.0.1 (May 26, 2021)
2
- Require minimum Ruby version of 2.5.0
3
-
4
- 1.0.0 (March 6, 2021)
5
- Commit to supporting latest patch versions of Ruby 2.5+
6
- Improve documentation
7
- Format code with StandardRB
8
- Bump gem dependencies
9
-
10
- 0.2.0 (May 12, 2017)
11
- Allow nested environment changes in the same thread
12
-
13
- 0.1.0 (January 7, 2017)
14
- Remove ActiveSupport dependency
15
-
16
- 0.0.4 (January 6, 2017)
17
- Improved thread safety
18
- Handle TypeErrors during assignment
19
- Improve documentation
20
-
21
- 0.0.1 (November 28, 2012)
22
- 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