├── .gitignore ├── RELEASE.md ├── Cargo.toml ├── .github ├── dependabot.yml └── workflows │ └── build.yml ├── LICENSE-MIT ├── README.md ├── LICENSE-APACHE └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | Release process 2 | =============== 3 | 4 | Create a pull request with a version bump to indicate the intention to do a release. This should give users the ability to give feedback, e.g. if a breaking change was missed and the version bump is incorrect. 5 | 6 | Once that PR is merged do the actual release with [`cargo-release`](https://github.com/crate-ci/cargo-release). 7 | 8 | This requires the following permissions 9 | 10 | - on github.com/vmx/temp-env 11 | - creating tags 12 | - pushing to `main` 13 | - on crates.io 14 | - publish access to all published crates 15 | 16 | Dry run 17 | 18 | ```console 19 | $ cargo release -vvv 20 | ``` 21 | 22 | Actual publishing 23 | 24 | ```console 25 | $ cargo release --execute 26 | ``` 27 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "temp-env" 3 | version = "0.3.6" 4 | authors = ["Volker Mische ", "Fabian Braun "] 5 | license = "MIT OR Apache-2.0" 6 | repository = "https://github.com/vmx/temp-env" 7 | description = "Set environment variables temporarily." 8 | keywords = ["env", "environment", "envvar", "temporary", "testing"] 9 | categories = ["development-tools", "development-tools::testing"] 10 | edition = "2021" 11 | rust-version = "1.63.0" 12 | 13 | [dependencies] 14 | futures = { version = "0.3.31", optional = true } 15 | parking_lot = { version = "0.12.3" } 16 | 17 | [dev-dependencies] 18 | tokio = { version = "1.38.1", features = ["full"]} 19 | 20 | [features] 21 | default = [] 22 | async_closure = ["dep:futures"] 23 | 24 | [package.metadata.docs.rs] 25 | all-features = true 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: monthly 12 | time: "09:00" 13 | timezone: Europe/Berlin 14 | groups: 15 | github-actions: 16 | patterns: 17 | - "*" 18 | open-pull-requests-limit: 99 19 | - package-ecosystem: cargo 20 | directory: "/" 21 | schedule: 22 | interval: monthly 23 | time: "09:00" 24 | timezone: Europe/Berlin 25 | groups: 26 | cargo: 27 | patterns: 28 | - "*" 29 | open-pull-requests-limit: 99 30 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Protocol Labs, Inc., Fabian Braun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | MSRV: 1.63.0 7 | 8 | jobs: 9 | msrv: 10 | name: MSRV 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Sources 14 | uses: actions/checkout@v4 15 | 16 | - name: Cache Dependencies & Build Outputs 17 | uses: actions/cache@v4 18 | with: 19 | path: ~/.cargo 20 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 21 | 22 | - name: Install Rust Toolchain 23 | uses: actions-rs/toolchain@v1 24 | with: 25 | profile: minimal 26 | toolchain: ${{ env.MSRV }} 27 | override: true 28 | 29 | - name: Test 30 | uses: actions-rs/cargo@v1 31 | with: 32 | command: build 33 | # Test with default features only, as the async support needs a newer 34 | # Rust version. 35 | args: --workspace 36 | 37 | build: 38 | name: Build 39 | strategy: 40 | fail-fast: false 41 | matrix: 42 | platform: [ubuntu-latest, macos-latest, windows-latest] 43 | toolchain: [stable] 44 | runs-on: ${{ matrix.platform }} 45 | 46 | steps: 47 | - name: Checkout Sources 48 | uses: actions/checkout@v4 49 | 50 | - name: Cache Dependencies & Build Outputs 51 | uses: actions/cache@v4 52 | with: 53 | path: ~/.cargo 54 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 55 | 56 | - name: Install Rust Toolchain 57 | uses: actions-rs/toolchain@v1 58 | with: 59 | profile: minimal 60 | toolchain: ${{ matrix.toolchain }} 61 | override: true 62 | components: rustfmt, clippy 63 | 64 | - name: Check Code Format 65 | uses: actions-rs/cargo@v1 66 | with: 67 | command: fmt 68 | args: --all -- --check 69 | 70 | - name: Code Lint 71 | uses: actions-rs/cargo@v1 72 | with: 73 | command: clippy 74 | args: --all-targets --all-features --workspace -- -D warnings 75 | 76 | - name: Code Lint Without Default Features 77 | uses: actions-rs/cargo@v1 78 | with: 79 | command: clippy 80 | args: --no-default-features --workspace -- -D warnings 81 | 82 | - name: Test 83 | uses: actions-rs/cargo@v1 84 | with: 85 | command: test 86 | args: --all-features --workspace 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | temp-env 2 | ======== 3 | 4 | Set environment variables temporarily. 5 | 6 | This crate is useful for testing with different environment variables that should not interfere when running `cargo test`. 7 | 8 | This code started as a small test helper written by [@fabian-braun] and [@nbaztec] and published by [@fabian-braun] 9 | on [StackOverflow]. [@vmx] found it useful and took the time to make it a proper crate. 10 | 11 | Examples 12 | -------- 13 | 14 | ```rust 15 | temp_env::with_var("MY_ENV_VAR", Some("production"), || { 16 | // Run some code where `MY_ENV_VAR` is set to `production`. 17 | }); 18 | 19 | 20 | temp_env::with_vars( 21 | [ 22 | ("FIRST", Some("Hello")), 23 | ("SECOND", Some("World!")), 24 | ], 25 | || { 26 | // Run some code where `FIRST` is set to `Hello` and `SECOND` is set to `World!`. 27 | } 28 | ); 29 | 30 | temp_env::with_vars( 31 | [ 32 | ("FIRST", Some("Hello")), 33 | ("SECOND", None), 34 | ], 35 | || { 36 | // Run some code where `FIRST` is set to `Hello` and `SECOND` is unset (even if 37 | // it was set before) 38 | } 39 | ); 40 | 41 | let value = temp_env::with_var("MY_ENV_VAR", Some("production"), || { 42 | // Compute a value in a closure while `MY_ENV_VAR` is set to `production`. 43 | let envvar = env::var("MY_ENV_VAR").unwrap(); 44 | if envvar == "production" { 45 | true 46 | } else { 47 | false 48 | } 49 | }); 50 | ``` 51 | 52 | How does it work? 53 | ------- 54 | 55 | This crate sets and unsets environment variables for the currently running (Rust) process. 56 | It leverages [`std::env::set_var`]. 57 | 58 | The provided functions `temp_env::with_*` provide the following features: 59 | - Avoid interference when running concurrently 60 | - Reset previously set env variables back to their original value upon completion, also in case of panic 61 | - Temporarily unsetting env variables 62 | 63 | Note that the crate makes use of a singleton mutex to avoid side effects between concurrently running tests. 64 | This may impact the degree of concurrency in your test execution. 65 | 66 | Features 67 | -------- 68 | 69 | - `async_closure`: When enabled you can use `async_with_var()` with async closures. This feature needs at least Rust version 1.64. 70 | 71 | Noteworthy 72 | ---------- 73 | 74 | As an alternative to using this crate, 75 | you might consider migrating your testing to [`cargo-nextest`](https://github.com/nextest-rs/nextest). 76 | `cargo-nextest` runs each test in a separate process, which makes it unnecessary to synchronize the altering of environment variables 77 | as described [here](https://nexte.st/docs/configuration/env-vars/#altering-the-environment-within-tests). 78 | 79 | License 80 | ------- 81 | 82 | This project is licensed under either of 83 | 84 | * Apache License, Version 2.0, ([LICENSE-APACHE]) or https://www.apache.org/licenses/LICENSE-2.0) 85 | * MIT license ([LICENSE-MIT] or https://opensource.org/licenses/MIT) 86 | 87 | at your option. 88 | 89 | 90 | [StackOverflow]: https://stackoverflow.com/questions/35858323/how-can-i-test-rust-methods-that-depend-on-environment-variables/67433684#67433684 91 | [@fabian-braun]: https://github.com/fabian-braun 92 | [@nbaztec]: https://github.com/nbaztec 93 | [@vmx]: https://github.com/vmx 94 | [LICENSE-APACHE]: LICENSE-APACHE 95 | [LICENSE-MIT]: LICENSE-MIT 96 | [`std::env::set_var`]: https://doc.rust-lang.org/std/env/fn.set_var.html 97 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | //! This crate is for setting environment variables temporarily. 3 | //! 4 | //! It is useful for testing with different environment variables that should not interfere. 5 | //! 6 | //! # Examples 7 | //! 8 | //! ```rust 9 | //! temp_env::with_var("MY_ENV_VAR", Some("production"), || { 10 | //! // Run some code where `MY_ENV_VAR` set to `"production"`. 11 | //! }); 12 | //! 13 | //! temp_env::with_vars( 14 | //! [ 15 | //! ("FIRST_VAR", Some("Hello")), 16 | //! ("SECOND_VAR", Some("World!")), 17 | //! ], 18 | //! || { 19 | //! // Run some code where `FIRST_VAR` is set to `"Hello"` and `SECOND_VAR` is set to 20 | //! // `"World!"`. 21 | //! } 22 | //! ); 23 | //! 24 | //! temp_env::with_vars( 25 | //! [ 26 | //! ("FIRST_VAR", Some("Hello")), 27 | //! ("SECOND_VAR", None), 28 | //! ], 29 | //! || { 30 | //! // Run some code where `FIRST_VAR` is set to `"Hello"` and `SECOND_VAR` is unset (even if 31 | //! // it was set before) 32 | //! } 33 | //! ); 34 | //! ``` 35 | //! 36 | //! It's possible the closure returns a value: 37 | //! 38 | //! ```rust 39 | //! let s = temp_env::with_var("INNER_ENV_VAR", Some("inner value"), || { 40 | //! std::env::var("INNER_ENV_VAR").unwrap() 41 | //! }); 42 | //! println!("{}", s); 43 | //! ``` 44 | //! 45 | 46 | use std::collections::HashMap; 47 | use std::env; 48 | use std::ffi::{OsStr, OsString}; 49 | use std::hash::Hash; 50 | 51 | use parking_lot::{ReentrantMutex, ReentrantMutexGuard}; 52 | 53 | /// Make sure that the environment isn't modified concurrently. 54 | static SERIAL_TEST: ReentrantMutex<()> = ReentrantMutex::new(()); 55 | 56 | /// Sets a single environment variable for the duration of the closure. 57 | /// 58 | /// The previous value is restored when the closure completes or panics, before unwinding the 59 | /// panic. 60 | /// 61 | /// If `value` is set to `None`, then the environment variable is unset. 62 | pub fn with_var(key: K, value: Option, closure: F) -> R 63 | where 64 | K: AsRef + Clone + Eq + Hash, 65 | V: AsRef + Clone, 66 | F: FnOnce() -> R, 67 | { 68 | with_vars([(key, value)], closure) 69 | } 70 | 71 | /// Unsets a single environment variable for the duration of the closure. 72 | /// 73 | /// The previous value is restored when the closure completes or panics, before unwinding the 74 | /// panic. 75 | /// 76 | /// This is a shorthand and identical to the following: 77 | /// ```rust 78 | /// temp_env::with_var("MY_ENV_VAR", None::<&str>, || { 79 | /// // Run some code where `MY_ENV_VAR` is unset. 80 | /// }); 81 | /// ``` 82 | pub fn with_var_unset(key: K, closure: F) -> R 83 | where 84 | K: AsRef + Clone + Eq + Hash, 85 | F: FnOnce() -> R, 86 | { 87 | with_var(key, None::<&str>, closure) 88 | } 89 | 90 | struct RestoreEnv<'a> { 91 | env: HashMap<&'a OsStr, Option>, 92 | _guard: ReentrantMutexGuard<'a, ()>, 93 | } 94 | 95 | impl<'a> RestoreEnv<'a> { 96 | /// Capture the given variables from the environment. 97 | /// 98 | /// `guard` holds a lock on the shared mutex for exclusive access to the environment, to make 99 | /// sure that the environment gets restored while the lock is still held, i.e the current 100 | /// thread still has exclusive access to the environment. 101 | fn capture(guard: ReentrantMutexGuard<'a, ()>, vars: I) -> Self 102 | where 103 | I: Iterator + 'a, 104 | { 105 | let env = vars.map(|v| (v, env::var_os(v))).collect(); 106 | Self { env, _guard: guard } 107 | } 108 | } 109 | 110 | impl Drop for RestoreEnv<'_> { 111 | fn drop(&mut self) { 112 | for (var, value) in self.env.iter() { 113 | update_env(var, value.as_ref().map(|v| v.as_os_str())); 114 | } 115 | } 116 | } 117 | 118 | /// Sets environment variables for the duration of the closure. 119 | /// 120 | /// The previous values are restored when the closure completes or panics, before unwinding the 121 | /// panic. 122 | /// 123 | /// If a `value` is set to `None`, then the environment variable is unset. 124 | /// 125 | /// If the variable with the same name is set multiple times, the last one wins. 126 | pub fn with_vars(kvs: impl AsRef<[(K, Option)]>, closure: F) -> R 127 | where 128 | K: AsRef + Clone + Eq + Hash, 129 | V: AsRef + Clone, 130 | F: FnOnce() -> R, 131 | { 132 | let old_env = RestoreEnv::capture( 133 | SERIAL_TEST.lock(), 134 | kvs.as_ref().iter().map(|(k, _)| k.as_ref()), 135 | ); 136 | for (key, value) in kvs.as_ref() { 137 | update_env(key, value.as_ref()); 138 | } 139 | let retval = closure(); 140 | drop(old_env); 141 | retval 142 | } 143 | 144 | /// Unsets environment variables for the duration of the closure. 145 | /// 146 | /// The previous values are restored when the closure completes or panics, before unwinding the 147 | /// panic. 148 | /// 149 | /// This is a shorthand and identical to the following: 150 | /// ```rust 151 | /// temp_env::with_vars( 152 | /// [ 153 | /// ("FIRST_VAR", None::<&str>), 154 | /// ("SECOND_VAR", None::<&str>), 155 | /// ], 156 | /// || { 157 | /// // Run some code where `FIRST_VAR` and `SECOND_VAR` are unset (even if 158 | /// // they were set before) 159 | /// } 160 | /// ); 161 | /// ``` 162 | pub fn with_vars_unset(keys: impl AsRef<[K]>, closure: F) -> R 163 | where 164 | K: AsRef + Clone + Eq + Hash, 165 | F: FnOnce() -> R, 166 | { 167 | let kvs = keys 168 | .as_ref() 169 | .iter() 170 | .map(|key| (key, None::<&str>)) 171 | .collect::>(); 172 | with_vars(kvs, closure) 173 | } 174 | 175 | fn update_env(key: K, value: Option) 176 | where 177 | K: AsRef, 178 | V: AsRef, 179 | { 180 | match value { 181 | Some(v) => env::set_var(key, v), 182 | None => env::remove_var(key), 183 | } 184 | } 185 | 186 | #[cfg(feature = "async_closure")] 187 | /// Does the same as [`with_vars`] but it allows to pass an async closures. 188 | /// 189 | /// ```rust 190 | /// async fn check_var() { 191 | /// let v = std::env::var("MY_VAR").unwrap(); 192 | /// assert_eq!(v, "ok".to_owned()); 193 | /// } 194 | /// 195 | /// #[cfg(feature = "async_closure")] 196 | /// #[tokio::test] 197 | /// async fn test_async_closure() { 198 | /// crate::async_with_vars([("MY_VAR", Some("ok"))], check_var()); 199 | /// } 200 | /// ``` 201 | pub async fn async_with_vars(kvs: impl AsRef<[(K, Option)]>, closure: F) -> R 202 | where 203 | K: AsRef + Clone + Eq + Hash, 204 | V: AsRef + Clone, 205 | F: std::future::Future + std::future::IntoFuture, 206 | { 207 | let old_env = RestoreEnv::capture( 208 | SERIAL_TEST.lock(), 209 | kvs.as_ref().iter().map(|(k, _)| k.as_ref()), 210 | ); 211 | for (key, value) in kvs.as_ref() { 212 | update_env(key, value.as_ref()); 213 | } 214 | let retval = closure.await; 215 | drop(old_env); 216 | retval 217 | } 218 | 219 | #[cfg(test)] 220 | mod tests { 221 | use std::env::VarError; 222 | use std::sync::atomic::{AtomicUsize, Ordering}; 223 | use std::{env, panic}; 224 | 225 | /// Generates unique String for use in tests below. 226 | /// (we need to prevent collision of environment variable keys to make the tests non-flaky). 227 | struct UniqueEnvKeyValGen { 228 | counter: AtomicUsize, 229 | } 230 | 231 | impl UniqueEnvKeyValGen { 232 | fn next(&self) -> String { 233 | let id = self.counter.fetch_add(1, Ordering::SeqCst); 234 | let key = format!("TEMP_ENV_KEY_{}", id); 235 | let actual_val = env::var(key.clone()); 236 | assert_eq!( 237 | Err(VarError::NotPresent), 238 | actual_val, 239 | "Test setup broken. {} environment variable is already set to {:?}", 240 | key, 241 | actual_val 242 | ); 243 | key.to_string() 244 | } 245 | } 246 | 247 | static GENERATOR: UniqueEnvKeyValGen = UniqueEnvKeyValGen { 248 | counter: AtomicUsize::new(0), 249 | }; 250 | 251 | /// Test setting an environment variable. 252 | #[test] 253 | fn test_with_var_set() { 254 | let env_key = &GENERATOR.next(); 255 | 256 | crate::with_var(env_key, Some(env_key), || { 257 | assert_eq!(env::var(env_key), Ok(env_key.to_string())); 258 | }); 259 | 260 | assert_eq!(env::var(env_key), Err(VarError::NotPresent)); 261 | } 262 | 263 | /// Test unsetting an environment variable. 264 | #[test] 265 | fn test_with_var_set_to_none() { 266 | let env_key = &GENERATOR.next(); 267 | env::set_var(env_key, env_key); 268 | 269 | crate::with_var(env_key, None::<&str>, || { 270 | assert_eq!(env::var(env_key), Err(VarError::NotPresent)); 271 | }); 272 | 273 | assert_eq!(env::var(env_key), Ok(env_key.to_string())); 274 | } 275 | 276 | /// Test setting an environment variable via shorthand. 277 | #[test] 278 | fn test_with_var_unset() { 279 | let env_key = &GENERATOR.next(); 280 | env::set_var(env_key, env_key); 281 | 282 | crate::with_var_unset(env_key, || { 283 | assert_eq!(env::var(env_key), Err(VarError::NotPresent)); 284 | }); 285 | 286 | assert_eq!(env::var(env_key), Ok(env_key.to_string())); 287 | } 288 | 289 | /// Test overriding an environment variable. 290 | #[test] 291 | fn test_with_var_override() { 292 | let env_key = &GENERATOR.next(); 293 | env::set_var(env_key, env_key); 294 | 295 | crate::with_var(env_key, Some("new"), || { 296 | assert_eq!(env::var(env_key), Ok("new".to_string())); 297 | }); 298 | 299 | assert_eq!(env::var(env_key), Ok(env_key.to_string())); 300 | } 301 | 302 | /// Test with_var panic behavior. 303 | #[test] 304 | fn test_with_var_panic() { 305 | let env_key = &GENERATOR.next(); 306 | env::set_var(env_key, env_key); 307 | 308 | let did_panic = panic::catch_unwind(|| { 309 | crate::with_var(env_key, Some("don't panic"), || { 310 | assert_eq!(env::var(env_key), Ok("don't panic".to_string())); 311 | panic!("abort this closure with a panic."); 312 | }); 313 | }); 314 | 315 | assert!(did_panic.is_err(), "The closure must panic."); 316 | assert_eq!(env::var(env_key), Ok(env_key.to_string())); 317 | } 318 | 319 | /// Test setting multiple environment variables. 320 | #[test] 321 | fn test_with_vars_set() { 322 | let env_key_1 = &GENERATOR.next(); 323 | let env_key_2 = &GENERATOR.next(); 324 | 325 | crate::with_vars( 326 | [(env_key_1, Some(env_key_1)), (env_key_2, Some(env_key_2))], 327 | || { 328 | assert_eq!(env::var(env_key_1), Ok(env_key_1.to_string())); 329 | assert_eq!(env::var(env_key_2), Ok(env_key_2.to_string())); 330 | }, 331 | ); 332 | 333 | assert_eq!(env::var(env_key_1), Err(VarError::NotPresent)); 334 | assert_eq!(env::var(env_key_2), Err(VarError::NotPresent)); 335 | } 336 | 337 | /// Test with_vars closure return behavior. 338 | #[test] 339 | fn test_with_vars_set_returning() { 340 | let env_key_1 = &GENERATOR.next(); 341 | let env_key_2 = &GENERATOR.next(); 342 | 343 | let r = crate::with_vars( 344 | [(env_key_1, Some(env_key_1)), (env_key_2, Some(env_key_2))], 345 | || { 346 | let one_is_set = env::var(env_key_1); 347 | let two_is_set = env::var(env_key_2); 348 | (one_is_set, two_is_set) 349 | }, 350 | ); 351 | 352 | let (one_from_closure, two_from_closure) = r; 353 | 354 | assert_eq!(one_from_closure, Ok(env_key_1.to_string())); 355 | assert_eq!(two_from_closure, Ok(env_key_2.to_string())); 356 | 357 | assert_eq!(env::var(env_key_1), Err(VarError::NotPresent)); 358 | assert_eq!(env::var(env_key_2), Err(VarError::NotPresent)); 359 | } 360 | 361 | /// Test unsetting multiple environment variables via shorthand. 362 | #[test] 363 | fn test_with_vars_unset() { 364 | let env_key_1 = &GENERATOR.next(); 365 | let env_key_2 = &GENERATOR.next(); 366 | env::set_var(env_key_1, env_key_1); 367 | 368 | crate::with_vars_unset([env_key_1, env_key_2], || { 369 | assert_eq!(env::var(env_key_1), Err(VarError::NotPresent)); 370 | assert_eq!(env::var(env_key_2), Err(VarError::NotPresent)); 371 | }); 372 | 373 | assert_eq!(env::var(env_key_1), Ok(env_key_1.to_string())); 374 | assert_eq!(env::var(env_key_2), Err(VarError::NotPresent)); 375 | } 376 | 377 | /// Test partially setting and unsetting environment variables. 378 | #[test] 379 | fn test_with_vars_partially_unset() { 380 | let env_key_1 = &GENERATOR.next(); 381 | let env_key_2 = &GENERATOR.next(); 382 | env::set_var(env_key_2, env_key_2); 383 | 384 | crate::with_vars( 385 | [(env_key_1, Some("set")), (env_key_2, None::<&str>)], 386 | || { 387 | assert_eq!(env::var(env_key_1), Ok("set".to_string())); 388 | assert_eq!(env::var(env_key_2), Err(VarError::NotPresent)); 389 | }, 390 | ); 391 | 392 | assert_eq!(env::var(env_key_1), Err(VarError::NotPresent)); 393 | assert_eq!(env::var(env_key_2), Ok(env_key_2.to_string())); 394 | } 395 | 396 | /// Test overriding multiple environment variables. 397 | #[test] 398 | fn test_with_vars_override() { 399 | let env_key_1 = &GENERATOR.next(); 400 | let env_key_2 = &GENERATOR.next(); 401 | env::set_var(env_key_1, env_key_1); 402 | env::set_var(env_key_2, env_key_2); 403 | 404 | crate::with_vars( 405 | [(env_key_1, Some("other")), (env_key_2, Some("value"))], 406 | || { 407 | assert_eq!(env::var(env_key_1), Ok("other".to_string())); 408 | assert_eq!(env::var(env_key_2), Ok("value".to_string())); 409 | }, 410 | ); 411 | 412 | assert_eq!(env::var(env_key_1), Ok(env_key_1.to_string())); 413 | assert_eq!(env::var(env_key_2), Ok(env_key_2.to_string())); 414 | } 415 | 416 | /// Test, when setting the same variable twice, the latter one is used. 417 | #[test] 418 | fn test_with_vars_same_vars() { 419 | let env_key = &GENERATOR.next(); 420 | 421 | crate::with_vars( 422 | [(env_key, Some("initial")), (env_key, Some("override"))], 423 | || { 424 | assert_eq!(env::var(env_key), Ok("override".to_string())); 425 | }, 426 | ); 427 | 428 | assert_eq!(env::var(env_key), Err(VarError::NotPresent)); 429 | } 430 | 431 | /// Test that unsetting and setting the same variable leads to the variable being set. 432 | #[test] 433 | fn test_with_vars_unset_set() { 434 | let env_key = &GENERATOR.next(); 435 | env::set_var(env_key, env_key); 436 | 437 | crate::with_vars( 438 | [(env_key, None::<&str>), (env_key, Some("new value"))], 439 | || { 440 | assert_eq!(env::var(env_key), Ok("new value".to_string())); 441 | }, 442 | ); 443 | 444 | assert_eq!(env::var(env_key), Ok(env_key.to_string())); 445 | } 446 | 447 | /// Test that setting and unsetting the same variable leads to the variable being unset. 448 | #[test] 449 | fn test_with_vars_set_unset() { 450 | let env_key = &GENERATOR.next(); 451 | 452 | crate::with_vars( 453 | [(env_key, Some("it is set")), (env_key, None::<&str>)], 454 | || { 455 | assert_eq!(env::var(env_key), Err(VarError::NotPresent)); 456 | }, 457 | ); 458 | 459 | assert_eq!(env::var(env_key), Err(VarError::NotPresent)); 460 | } 461 | 462 | #[test] 463 | fn test_with_nested_set() { 464 | let env_key_1 = &GENERATOR.next(); 465 | let env_key_2 = &GENERATOR.next(); 466 | 467 | crate::with_var(env_key_1, Some(env_key_1), || { 468 | crate::with_var(env_key_2, Some(env_key_2), || { 469 | assert_eq!(env::var(env_key_1), Ok(env_key_1.to_string())); 470 | assert_eq!(env::var(env_key_2), Ok(env_key_2.to_string())); 471 | }) 472 | }); 473 | 474 | assert_eq!(env::var(env_key_1), Err(VarError::NotPresent)); 475 | assert_eq!(env::var(env_key_2), Err(VarError::NotPresent)); 476 | } 477 | 478 | #[test] 479 | fn test_fn_once() { 480 | let env_key = &GENERATOR.next(); 481 | let value = String::from("Hello, "); 482 | let value = crate::with_var(env_key, Some("world!"), || { 483 | value + &env::var(env_key).unwrap() 484 | }); 485 | assert_eq!(value, "Hello, world!"); 486 | } 487 | 488 | #[cfg(feature = "async_closure")] 489 | async fn check_var(env_key: &str) { 490 | let v = env::var(env_key).unwrap(); 491 | assert_eq!(v, "ok".to_owned()); 492 | } 493 | 494 | #[cfg(feature = "async_closure")] 495 | #[tokio::test] 496 | async fn test_async_closure() { 497 | let env_key = &GENERATOR.next(); 498 | crate::async_with_vars([(env_key, Some("ok"))], check_var(env_key)).await; 499 | 500 | let f = async { 501 | let v = env::var(env_key).unwrap(); 502 | assert_eq!(v, "ok".to_owned()); 503 | }; 504 | 505 | crate::async_with_vars([(env_key, Some("ok"))], f).await; 506 | } 507 | 508 | #[cfg(feature = "async_closure")] 509 | #[tokio::test(flavor = "multi_thread")] 510 | async fn test_async_closure_calls_closure() { 511 | let env_key = &GENERATOR.next(); 512 | let (tx, rx) = tokio::sync::oneshot::channel(); 513 | let f = async { 514 | tx.send(env::var(env_key)).unwrap(); 515 | }; 516 | crate::async_with_vars([(env_key, Some("ok"))], f).await; 517 | let value = rx.await.unwrap().unwrap(); 518 | assert_eq!(value, "ok".to_owned()); 519 | } 520 | 521 | #[cfg(feature = "async_closure")] 522 | #[tokio::test(flavor = "multi_thread")] 523 | async fn test_async_with_vars_set_returning() { 524 | let env_key_1 = &GENERATOR.next(); 525 | let env_key_2 = &GENERATOR.next(); 526 | 527 | let r = crate::async_with_vars( 528 | [(env_key_1, Some(env_key_1)), (env_key_2, Some(env_key_2))], 529 | async { 530 | let one_is_set = env::var(env_key_1).unwrap(); 531 | let two_is_set = env::var(env_key_2).unwrap(); 532 | (one_is_set, two_is_set) 533 | }, 534 | ) 535 | .await; 536 | 537 | let (one_from_closure, two_from_closure) = r; 538 | assert_eq!(one_from_closure, env_key_1.to_string()); 539 | assert_eq!(two_from_closure, env_key_2.to_string()); 540 | assert_eq!(env::var(env_key_1), Err(VarError::NotPresent)); 541 | assert_eq!(env::var(env_key_2), Err(VarError::NotPresent)); 542 | } 543 | } 544 | --------------------------------------------------------------------------------