climate_control 1.0.0 → 1.1.1

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