climate_control-1.2.0/0000755000004100000410000000000014265511233014722 5ustar www-datawww-dataclimate_control-1.2.0/CODEOWNERS0000644000004100000410000000002114265511233016306 0ustar www-datawww-data* @dorianmariefr climate_control-1.2.0/README.md0000644000004100000410000001107114265511233016201 0ustar www-datawww-data# Climate Control ![GitHub Actions CI](https://github.com/thoughtbot/climate_control/actions/workflows/ci.yml/badge.svg) Easily manage your environment. ## Installation Add this line to your application's Gemfile: gem 'climate_control' And then execute: $ bundle Or install it yourself as: $ gem install climate_control ## Usage Climate Control can be used to temporarily assign environment variables within a block: ```ruby ClimateControl.modify CONFIRMATION_INSTRUCTIONS_BCC: 'confirmation_bcc@example.com' do sign_up_as 'john@example.com' confirm_account_for_email 'john@example.com' expect(current_email).to bcc_to('confirmation_bcc@example.com') end ``` To modify multiple environment variables: ```ruby ClimateControl.modify CONFIRMATION_INSTRUCTIONS_BCC: 'confirmation_bcc@example.com', MAIL_FROM: 'us@example.com' do sign_up_as 'john@example.com' confirm_account_for_email 'john@example.com' expect(current_email).to bcc_to('confirmation_bcc@example.com') expect(current_email).to be_from('us@example.com') end ``` To use with RSpec, you could define this in your spec: ```ruby def with_modified_env(options = {}, &block) ClimateControl.modify(options, &block) end ``` This would allow for more straightforward way to modify the environment: ```ruby require 'spec_helper' describe Thing, 'name' do it 'appends ADDITIONAL_NAME' do with_modified_env ADDITIONAL_NAME: 'bar' do expect(Thing.new.name).to eq('John Doe Bar') end end def with_modified_env(options, &block) ClimateControl.modify(options, &block) end end ``` To modify the environment for an entire set of tests in RSpec, use an `around` block: ```ruby describe Thing, 'name' do # ... tests around do |example| ClimateControl.modify FOO: 'bar' do example.run end end end ``` Environment variables assigned within the block will be preserved; essentially, the code should behave exactly the same with and without the block, except for the overrides. Transparency is crucial because the code executed within the block is not for `ClimateControl` to manage or modify. See the tests for more detail about the specific behaviors. ## Why Use Climate Control? By following guidelines regarding environment variables outlined by the [twelve-factor app](http://12factor.net/config), testing code in an isolated manner becomes more difficult: * avoiding modifications and testing values, we introduce mystery guests * making modifications and testing values, we introduce risk as environment variables represent global state Climate Control modifies environment variables only within the context of the block, ensuring values are managed properly and consistently. ## Thread-safety When using threads, for instance when running tests concurrently in the same process, you may need to wrap your code inside `ClimateControl.modify` blocks, e.g.: ```ruby first_thread = Thread.new do ClimateControl.modify(SECRET: "1") do p ENV["SECRET"] # => "1" sleep 2 p ENV["SECRET"] # => "1" end end second_thread = Thread.new do ClimateControl.modify({}) do sleep 1 p ENV["SECRET"] # => nil sleep 1 p ENV["SECRET"] # => nil end end first_thread.join second_thread.join ``` > 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). > > Josh Clayton ## Contributing 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request This project uses [StandardRB](https://github.com/testdouble/standard) to ensure formatting. ## License 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. About thoughtbot ---------------- ![thoughtbot](https://thoughtbot.com/brand_assets/93:44.svg) climate_control is maintained and funded by thoughtbot, inc. The names and logos for thoughtbot are trademarks of thoughtbot, inc. We love open source software! See [our other projects][community] or [hire us][hire] to design, develop, and grow your product. [community]: https://thoughtbot.com/community?utm_source=github [hire]: https://thoughtbot.com/hire-us?utm_source=github climate_control-1.2.0/climate_control.gemspec0000644000004100000410000000157414265511233021454 0ustar www-datawww-datalib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "climate_control/version" Gem::Specification.new do |gem| gem.name = "climate_control" gem.version = ClimateControl::VERSION gem.authors = ["Joshua Clayton", "Dorian MariƩ"] gem.email = ["joshua.clayton@gmail.com", "dorian@dorianmarie.fr"] gem.description = "Modify your ENV" gem.summary = "Modify your ENV easily with ClimateControl" gem.homepage = "https://github.com/thoughtbot/climate_control" gem.license = "MIT" gem.files = `git ls-files`.split($/) gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.require_paths = ["lib"] gem.required_ruby_version = ">= 2.5.0" gem.add_development_dependency "rspec" gem.add_development_dependency "rake" gem.add_development_dependency "simplecov" gem.add_development_dependency "standard" end climate_control-1.2.0/spec/0000755000004100000410000000000014265511233015654 5ustar www-datawww-dataclimate_control-1.2.0/spec/acceptance/0000755000004100000410000000000014265511233017742 5ustar www-datawww-dataclimate_control-1.2.0/spec/acceptance/climate_control_spec.rb0000644000004100000410000001320014265511233024453 0ustar www-datawww-datarequire "spec_helper" Thing = Class.new describe "Climate control" do it "allows modification of the environment" do block_run = false with_modified_env FOO: "bar" do expect(ENV["FOO"]).to eq "bar" block_run = true end expect(ENV["FOO"]).to be_nil expect(block_run).to be true end it "modifies the environment" do with_modified_env VARIABLE_1: "bar", VARIABLE_2: "qux" do expect(ENV["VARIABLE_1"]).to eq "bar" expect(ENV["VARIABLE_2"]).to eq "qux" end expect(ENV["VARIABLE_1"]).to be_nil expect(ENV["VARIABLE_2"]).to be_nil end it "allows for environment variables to be assigned within the block" do with_modified_env VARIABLE_1: "modified" do ENV["ASSIGNED_IN_BLOCK"] = "assigned" end expect(ENV["ASSIGNED_IN_BLOCK"]).to eq "assigned" end it "reassigns previously set environment variables" do ENV["VARIABLE_ASSIGNED_BEFORE_MODIFYING_ENV"] = "original" expect(ENV["VARIABLE_ASSIGNED_BEFORE_MODIFYING_ENV"]).to eq "original" with_modified_env VARIABLE_ASSIGNED_BEFORE_MODIFYING_ENV: "overridden" do expect(ENV["VARIABLE_ASSIGNED_BEFORE_MODIFYING_ENV"]).to eq "overridden" end expect(ENV["VARIABLE_ASSIGNED_BEFORE_MODIFYING_ENV"]).to eq "original" end it "persists the change when overriding the variable in the block" do with_modified_env VARIABLE_MODIFIED_AND_THEN_ASSIGNED: "modified" do ENV["VARIABLE_MODIFIED_AND_THEN_ASSIGNED"] = "assigned value" end expect(ENV["VARIABLE_MODIFIED_AND_THEN_ASSIGNED"]).to eq "assigned value" end it "resets environment variables even if the block raises" do expect { with_modified_env FOO: "bar" do raise "broken" end }.to raise_error("broken") expect(ENV["FOO"]).to be_nil end it "preserves environment variables set within the block" do ENV["CHANGED"] = "old value" with_modified_env IRRELEVANT: "ignored value" do ENV["CHANGED"] = "new value" end expect(ENV["CHANGED"]).to eq "new value" end it "returns the value of the block" do value = with_modified_env VARIABLE_1: "bar" do "value inside block" end expect(value).to eq "value inside block" end it "handles threads correctly" do # failure path without mutex # [thread_removing_env] BAZ is assigned # 0.25s passes # [other_thread] FOO is assigned and ENV is copied (which includes BAZ) # 0.25s passes # [thread_removing_env] thread resolves and BAZ is removed from env; other_thread still retains knowledge of BAZ # 0.25s passes # [other_thread] thread resolves, FOO is removed, BAZ is copied back to ENV thread_removing_env = Thread.new { with_modified_env BAZ: "buzz" do sleep 0.5 end expect(ENV["BAZ"]).to be_nil } other_thread = Thread.new { sleep 0.25 with_modified_env FOO: "bar" do sleep 0.5 end expect(ENV["FOO"]).to be_nil } thread_removing_env.join other_thread.join expect(ENV["FOO"]).to be_nil expect(ENV["BAZ"]).to be_nil end it "handles threads accessing the same key wrapped in a block" do first_thread = Thread.new { with_modified_env do with_modified_env CONFLICTING_KEY: "1" do sleep 0.5 expect(ENV["CONFLICTING_KEY"]).to eq("1") end expect(ENV["CONFLICTING_KEY"]).to be_nil end } second_thread = Thread.new { with_modified_env do sleep 0.25 expect(ENV["CONFLICTING_KEY"]).to be_nil with_modified_env CONFLICTING_KEY: "2" do expect(ENV["CONFLICTING_KEY"]).to eq("2") sleep 0.5 expect(ENV["CONFLICTING_KEY"]).to eq("2") end expect(ENV["CONFLICTING_KEY"]).to be_nil end } first_thread.join second_thread.join expect(ENV["CONFLICTING_KEY"]).to be_nil end it "is re-entrant" do ret = with_modified_env(FOO: "foo") { with_modified_env(BAR: "bar") do "bar" end } expect(ret).to eq("bar") expect(ENV["FOO"]).to be_nil expect(ENV["BAR"]).to be_nil end it "raises when the value cannot be assigned properly" do message = generate_type_error_for_object(Thing.new) expect { with_modified_env(FOO: Thing.new) }.to raise_error ClimateControl::UnassignableValueError, /attempted to assign .*Thing.* to FOO but failed \(#{message}\)$/ end it "restores the ENV even when an error was raised when assigning values" do ENV["KEY_TO_OVERRIDE"] = "initial_value_1" ENV["KEY_THAT_WILL_ERROR_OUT"] = "initial_value_2" expect { with_modified_env( KEY_TO_OVERRIDE: "overwriten_value_1", KEY_THAT_WILL_ERROR_OUT: :value_that_will_error_out ) {} }.to raise_error ClimateControl::UnassignableValueError expect(ENV["KEY_TO_OVERRIDE"]).to eq("initial_value_1") expect(ENV["KEY_THAT_WILL_ERROR_OUT"]).to eq("initial_value_2") end it "doesn't block on nested modify calls" do with_modified_env(SMS_DEFAULT_COUNTRY_CODE: nil) do with_modified_env(SMS_DEFAULT_COUNTRY_CODE: "++56") do expect(ENV.fetch("SMS_DEFAULT_COUNTRY_CODE", "++41")).to eq("++56") end expect(ENV.fetch("SMS_DEFAULT_COUNTRY_CODE", "++41")).to eq("++41") end end def with_modified_env(options = {}, &block) ClimateControl.modify(options, &block) end def generate_type_error_for_object(object) message = nil begin "1" + object rescue TypeError => e message = e.message end message end around do |example| old_env = ENV.to_hash example.run ENV.clear old_env.each do |key, value| ENV[key] = value end end end climate_control-1.2.0/spec/acceptance/unsafe_modify_spec.rb0000644000004100000410000001153114265511233024132 0ustar www-datawww-datarequire "spec_helper" describe "ClimateControl#unsafe_modify" do it "allows modification of the environment" do block_run = false with_modified_env FOO: "bar" do expect(ENV["FOO"]).to eq "bar" block_run = true end expect(ENV["FOO"]).to be_nil expect(block_run).to be true end it "modifies the environment" do with_modified_env VARIABLE_1: "bar", VARIABLE_2: "qux" do expect(ENV["VARIABLE_1"]).to eq "bar" expect(ENV["VARIABLE_2"]).to eq "qux" end expect(ENV["VARIABLE_1"]).to be_nil expect(ENV["VARIABLE_2"]).to be_nil end it "allows for environment variables to be assigned within the block" do with_modified_env VARIABLE_1: "modified" do ENV["ASSIGNED_IN_BLOCK"] = "assigned" end expect(ENV["ASSIGNED_IN_BLOCK"]).to eq "assigned" end it "reassigns previously set environment variables" do ENV["VARIABLE_ASSIGNED_BEFORE_MODIFYING_ENV"] = "original" expect(ENV["VARIABLE_ASSIGNED_BEFORE_MODIFYING_ENV"]).to eq "original" with_modified_env VARIABLE_ASSIGNED_BEFORE_MODIFYING_ENV: "overridden" do expect(ENV["VARIABLE_ASSIGNED_BEFORE_MODIFYING_ENV"]).to eq "overridden" end expect(ENV["VARIABLE_ASSIGNED_BEFORE_MODIFYING_ENV"]).to eq "original" end it "persists the change when overriding the variable in the block" do with_modified_env VARIABLE_MODIFIED_AND_THEN_ASSIGNED: "modified" do ENV["VARIABLE_MODIFIED_AND_THEN_ASSIGNED"] = "assigned value" end expect(ENV["VARIABLE_MODIFIED_AND_THEN_ASSIGNED"]).to eq "assigned value" end it "resets environment variables even if the block raises" do expect { with_modified_env FOO: "bar" do raise "broken" end }.to raise_error("broken") expect(ENV["FOO"]).to be_nil end it "preserves environment variables set within the block" do ENV["CHANGED"] = "old value" with_modified_env IRRELEVANT: "ignored value" do ENV["CHANGED"] = "new value" end expect(ENV["CHANGED"]).to eq "new value" end it "returns the value of the block" do value = with_modified_env VARIABLE_1: "bar" do "value inside block" end expect(value).to eq "value inside block" end it "handles threads correctly" do # failure path without mutex # [thread_removing_env] BAZ is assigned # 0.25s passes # [other_thread] FOO is assigned and ENV is copied (which includes BAZ) # 0.25s passes # [thread_removing_env] thread resolves and BAZ is removed from env; other_thread still retains knowledge of BAZ # 0.25s passes # [other_thread] thread resolves, FOO is removed, BAZ is copied back to ENV thread_removing_env = Thread.new { with_modified_env BAZ: "buzz" do sleep 0.5 end expect(ENV["BAZ"]).to be_nil } other_thread = Thread.new { sleep 0.25 with_modified_env FOO: "bar" do sleep 0.5 end expect(ENV["FOO"]).to be_nil } thread_removing_env.join other_thread.join expect(ENV["FOO"]).to be_nil expect(ENV["BAZ"]).to be_nil end it "is re-entrant" do ret = with_modified_env(FOO: "foo") { with_modified_env(BAR: "bar") do "bar" end } expect(ret).to eq("bar") expect(ENV["FOO"]).to be_nil expect(ENV["BAR"]).to be_nil end it "raises when the value cannot be assigned properly" do message = generate_type_error_for_object(Thing.new) expect { with_modified_env(FOO: Thing.new) }.to raise_error ClimateControl::UnassignableValueError, /attempted to assign .*Thing.* to FOO but failed \(#{message}\)$/ end it "restores the ENV even when an error was raised when assigning values" do ENV["KEY_TO_OVERRIDE"] = "initial_value_1" ENV["KEY_THAT_WILL_ERROR_OUT"] = "initial_value_2" expect { with_modified_env( KEY_TO_OVERRIDE: "overwriten_value_1", KEY_THAT_WILL_ERROR_OUT: :value_that_will_error_out ) {} }.to raise_error ClimateControl::UnassignableValueError expect(ENV["KEY_TO_OVERRIDE"]).to eq("initial_value_1") expect(ENV["KEY_THAT_WILL_ERROR_OUT"]).to eq("initial_value_2") end it "doesn't block on nested modify calls" do with_modified_env(SMS_DEFAULT_COUNTRY_CODE: nil) do with_modified_env(SMS_DEFAULT_COUNTRY_CODE: "++56") do expect(ENV.fetch("SMS_DEFAULT_COUNTRY_CODE", "++41")).to eq("++56") end expect(ENV.fetch("SMS_DEFAULT_COUNTRY_CODE", "++41")).to eq("++41") end end def with_modified_env(options = {}, &block) ClimateControl.unsafe_modify(options, &block) end def generate_type_error_for_object(object) message = nil begin "1" + object rescue TypeError => e message = e.message end message end around do |example| old_env = ENV.to_hash example.run ENV.clear old_env.each do |key, value| ENV[key] = value end end end climate_control-1.2.0/spec/spec_helper.rb0000644000004100000410000000045414265511233020475 0ustar www-datawww-databegin require "simplecov" SimpleCov.start rescue LoadError warn "warning: simplecov gem not found; skipping coverage" end $LOAD_PATH << File.join(File.dirname(__FILE__), "..", "lib") $LOAD_PATH << File.join(File.dirname(__FILE__)) require "rubygems" require "rspec" require "climate_control" climate_control-1.2.0/CHANGELOG.md0000644000004100000410000000251714265511233016540 0ustar www-datawww-data# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## 1.2.0 / 2022-07-15 - Added: `ClimateControl.unsafe_modify` for a thread-unsafe version of `ClimateControl.modify` (useful for minitest-around for instance) - Deprecates `ClimateControl.env`, `ENV` should be used instead ## 1.1.1 / 2022-05-28 - Fixed: ENV was not restored if an error was thrown when assigning ENV ## 1.1.0 / 2022-05-26 - Refactor to use `Monitor` instead of `Mutex` - Add documentation about thread-safety - Allow ClimateControl.modify to be called without environment variables - Add test for concurrent access needed to be inside block - Relax development dependencies ## 1.0.1 / 2021-05-26 - Require minimum Ruby version of 2.5.0 # 1.0.0 / 2021-03-06 - Commit to supporting latest patch versions of Ruby 2.5+ - Improve documentation - Format code with StandardRB - Bump gem dependencies # 0.2.0 / 2017-05-12 - Allow nested environment changes in the same thread # 0.1.0 / 2017-01-07 - Remove ActiveSupport dependency # 0.0.4 / 2017-01-06 - Improved thread safety - Handle TypeErrors during assignment - Improve documentation # 0.0.1 / 2012-11-28 - Initial release climate_control-1.2.0/.gitignore0000644000004100000410000000023614265511233016713 0ustar www-datawww-data*.gem *.rbc bin .bundle .config .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp climate_control-1.2.0/CODE_OF_CONDUCT.md0000644000004100000410000000025114265511233017517 0ustar www-datawww-data# Code of Conduct By participating in this project, you agree to abide by the [thoughtbot code of conduct][1]. [1]: https://thoughtbot.com/open-source-code-of-conduct climate_control-1.2.0/LICENSE0000644000004100000410000000211114265511233015722 0ustar www-datawww-dataCopyright (c) 2012-2021 Joshua Clayton and thoughtbot, inc. MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. climate_control-1.2.0/Rakefile0000644000004100000410000000016414265511233016370 0ustar www-datawww-datarequire "bundler/gem_tasks" require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:rspec) task default: :rspec climate_control-1.2.0/lib/0000755000004100000410000000000014265511233015470 5ustar www-datawww-dataclimate_control-1.2.0/lib/climate_control.rb0000644000004100000410000000275214265511233021201 0ustar www-datawww-datarequire "climate_control/errors" require "climate_control/version" require "monitor" module ClimateControl extend self extend Gem::Deprecate SEMAPHORE = Monitor.new private_constant :SEMAPHORE def modify(environment_overrides = {}, &block) environment_overrides = environment_overrides.transform_keys(&:to_s) SEMAPHORE.synchronize do previous = ENV.to_hash begin copy environment_overrides ensure middle = ENV.to_hash end block.call ensure after = ENV (previous.keys | middle.keys | after.keys).each do |key| if previous[key] != after[key] && middle[key] == after[key] ENV[key] = previous[key] end end end end def unsafe_modify(environment_overrides = {}, &block) environment_overrides = environment_overrides.transform_keys(&:to_s) previous = ENV.to_hash begin copy environment_overrides ensure middle = ENV.to_hash end block.call ensure after = ENV (previous.keys | middle.keys | after.keys).each do |key| if previous[key] != after[key] && middle[key] == after[key] ENV[key] = previous[key] end end end def env ENV end deprecate :env, "ENV", 2022, 10 private def copy(overrides) overrides.each do |key, value| ENV[key] = value rescue TypeError => e raise UnassignableValueError, "attempted to assign #{value} to #{key} but failed (#{e.message})" end end end climate_control-1.2.0/lib/climate_control/0000755000004100000410000000000014265511233020646 5ustar www-datawww-dataclimate_control-1.2.0/lib/climate_control/version.rb0000644000004100000410000000006514265511233022661 0ustar www-datawww-datamodule ClimateControl VERSION = "1.2.0".freeze end climate_control-1.2.0/lib/climate_control/errors.rb0000644000004100000410000000011614265511233022505 0ustar www-datawww-datamodule ClimateControl class UnassignableValueError < ArgumentError; end end climate_control-1.2.0/Gemfile0000644000004100000410000000014414265511233016214 0ustar www-datawww-datasource "https://rubygems.org" # Specify your gem's dependencies in climate_control.gemspec gemspec climate_control-1.2.0/.github/0000755000004100000410000000000014265511233016262 5ustar www-datawww-dataclimate_control-1.2.0/.github/workflows/0000755000004100000410000000000014265511233020317 5ustar www-datawww-dataclimate_control-1.2.0/.github/workflows/ci.yml0000644000004100000410000000114014265511233021431 0ustar www-datawww-dataname: CI on: [pull_request] jobs: tests: name: Tests runs-on: ubuntu-latest strategy: matrix: ruby-version: [3.1, '3.0', 2.7, 2.6, 2.5] steps: - uses: actions/checkout@v2 - name: Set up Ruby ${{ matrix.ruby-version }} uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} - name: Install gems run: | bundle config path vendor/bundle bundle install --jobs 4 --retry 3 - name: Run tests run: bundle exec rake - name: Run StandardRB run: bundle exec standardrb