climate_control 1.0.1 → 1.1.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
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