Skip to content

Commit 88b603b

Browse files
author
Tilmann Meyer
authored
test: introduce env variable mocking (starship#1490)
1 parent 8b0f589 commit 88b603b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

87 files changed

+4619
-4688
lines changed

.github/workflows/workflow.yml

-6
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,6 @@ jobs:
125125
profile: minimal
126126
override: true
127127

128-
# Install dotnet at a fixed version
129-
- name: Setup | DotNet
130-
uses: actions/setup-dotnet@v1
131-
with:
132-
dotnet-version: "2.2.402"
133-
134128
# Install Mercurial (pre-installed on Linux and windows)
135129
- name: Setup | Mercurial (macos)
136130
if: matrix.os == 'macOS-latest'

CONTRIBUTING.md

+117-24
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,52 @@ The project begins in [`main.rs`](src/main.rs), where the appropriate `print::`
2424

2525
Any styling that is applied to a module is inherited by its segments. Module prefixes and suffixes by default don't have any styling applied to them.
2626

27+
## Environment Variables and external commands
28+
29+
We have custom functions to be able to test our modules better. Here we show you how.
30+
31+
### Environment Variables
32+
33+
To get an environment variable we have special function to allow for mocking of vars. Here's a quick example:
34+
35+
```rust
36+
use super::{Context, Module, RootModuleConfig};
37+
38+
use crate::configs::php::PhpConfig;
39+
use crate::formatter::StringFormatter;
40+
use crate::utils;
41+
42+
43+
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
44+
// Here `my_env_var` will be either the contents of the var or the function
45+
// will exit if the variable is not set.
46+
let my_env_var = context.get_env("MY_VAR")?;
47+
48+
// Then you can happily use the value
49+
}
50+
```
51+
52+
## External commands
53+
54+
To run a external command (e.g. to get the version of a tool) and to allow for mocking use the `utils::exec_cmd` function. Here's a quick example:
55+
56+
```rust
57+
use super::{Context, Module, RootModuleConfig};
58+
59+
use crate::configs::php::PhpConfig;
60+
use crate::formatter::StringFormatter;
61+
use crate::utils;
62+
63+
64+
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
65+
// Here `my_env_var` will be either the stdout of the called command or the function
66+
// will exit if the called program was not installed or could not be run.
67+
let output = utils::exec_cmd("my_command", &["first_arg", "second_arg"])?.stdout;
68+
69+
// Then you can happily use the output
70+
}
71+
```
72+
2773
## Logging
2874

2975
Debug logging in starship is done with [pretty_env_logger](https://crates.io/crates/pretty_env_logger).
@@ -56,37 +102,81 @@ rustup component add rustfmt
56102
cargo fmt
57103
```
58104

59-
60105
## Testing
61106

62107
Testing is critical to making sure starship works as intended on systems big and small. Starship interfaces with many applications and system APIs when generating the prompt, so there's a lot of room for bugs to slip in.
63108

64-
Unit tests and a subset of integration tests can be run with `cargo test`.
65-
The full integration test suite is run on GitHub as part of our GitHub Actions continuous integration.
109+
Unit tests are written using the built-in Rust testing library in the same file as the implementation, as is traditionally done in Rust codebases. These tests can be run with `cargo test` and are run on GitHub as part of our GitHub Actions continuous integration to ensure consistend behavior.
110+
111+
All tests that test the rendered output of a module should use `ModuleRenderer`. For Example:
112+
113+
```rust
114+
use super::{Context, Module, RootModuleConfig};
115+
116+
use crate::configs::php::PhpConfig;
117+
use crate::formatter::StringFormatter;
118+
use crate::utils;
119+
120+
121+
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
122+
/* This is where your module code goes */
123+
}
124+
125+
#[cfg(test)]
126+
mod tests {
127+
use super::*;
128+
use crate::test::ModuleRenderer;
129+
use ansi_term::Color;
130+
use std::fs::File;
131+
use std::io;
132+
133+
134+
#[test]
135+
fn should_render() -> io::Result<()> {
136+
// Here you setup the testing environment
137+
let tempdir = tempfile::tempdir()?;
138+
// Create some file needed to render the module
139+
File::create(dir.path().join("YOUR_FILE"))?.sync_all()?;
140+
141+
// The output of the module
142+
let actual = ModuleRenderer::new("YOUR_MODULE_NAME")
143+
// For a custom path
144+
.path(&tempdir.path())
145+
// For a custom config
146+
.config(toml::toml!{
147+
[YOUR_MODULE_NAME]
148+
val = 1
149+
})
150+
// For env mocking
151+
.env("KEY","VALUE")
152+
// Run the module and collect the output
153+
.collect();
154+
155+
// The value that should be rendered by the module.
156+
let expected = Some(format!("{} ",Color::Black.paint("THIS SHOULD BE RENDERED")));
157+
158+
// Assert that the actual and expected values are the same
159+
assert_eq!(actual, expected);
160+
161+
// Close the tempdir
162+
tempdir.close()
163+
}
164+
}
165+
```
66166

67-
### Unit Testing
167+
If a module depends on output of another program, then that output should be added to the match statement in [`utils.rs`](src/utils.rs). The match has to be exactly the same as the call to `utils::exec_cmd()`, including positional arguments and flags. The array of arguments are joined by a `" "`, so `utils::exec_cmd("program", &["arg", "more_args"])` would match with the `program arg more_args` match statement.
68168

69-
Unit tests are written using the built-in Rust testing library in the same file as the implementation, as is traditionally done in Rust codebases. These tests can be run with `cargo test`.
169+
If the program cannot be mocked (e.g. It performs some filesystem operations, either writing or reading files) then it has to added to the project's GitHub Actions workflow file([`.github/workflows/workflow.yml`](.github/workflows/workflow.yml)) and the test has to be marked with an `#[ignored]`. This ensures that anyone can run the test suite locally without needing to pre-configure their environment. The `#[ignored]` attribute is bypassed during CI runs in GitHub Actions.
70170

71171
Unit tests should be fully isolated, only testing a given function's expected output given a specific input, and should be reproducible on any machine. Unit tests should not expect the computer running them to be in any particular state. This includes having any applications pre-installed, having any environment variables set, etc.
72172

73173
The previous point should be emphasized: even seemingly innocuous ideas like "if we can see the directory, we can read it" or "nobody will have their home directory be a git repo" have bitten us in the past. Having even a single test fail can completely break installation on some platforms, so be careful with tests!
74174

75-
### Integration Testing
76-
77-
Integration tests are located in the [`tests/`](tests) directory and are also written using the built-in Rust testing library.
78-
79-
Integration tests should test full modules or the entire prompt. All integration tests that expect the testing environment to have pre-existing state or tests that make permanent changes to the filesystem should have the `#[ignore]` attribute added to them. All tests that don't depend on any preexisting state will be run alongside the unit tests with `cargo test`.
80-
81-
For tests that depend on having preexisting state, whatever needed state will have to be added to the project's GitHub Actions workflow file([`.github/workflows/workflow.yml`](.github/workflows/workflow.yml)).
82-
83175
### Test Programming Guidelines
84176

85177
Any tests that depend on File I/O should use [`sync_all()`](https://doc.rust-lang.org/std/fs/struct.File.html#method.sync_all) when creating files or after writing to files.
86178

87-
Any tests that use `tempfile::tempdir` should take care to call `dir.close()` after usage to ensure the lifecycle of the directory can be reasoned about.
88-
89-
Any tests that use `create_fixture_repo()` should remove the returned directory after usage with `remove_dir_all::remove_dir_all()`.
179+
Any tests that use `tempfile::tempdir` should take care to call `dir.close()` after usage to ensure the lifecycle of the directory can be reasoned about. This includes `fixture_repo()` as it returns a TempDir that should be closed.
90180

91181
## Running the Documentation Website Locally
92182

@@ -98,17 +188,20 @@ After cloning the project, you can do the following to run the VuePress website
98188

99189
1. `cd` into the `/docs` directory.
100190
2. Install the project dependencies:
101-
```
102-
$ npm install
103-
```
191+
192+
```sh
193+
npm install
194+
```
195+
104196
3. Start the project in development mode:
105-
```
106-
$ npm run dev
107-
```
108197

109-
Once setup is complete, you can refer to VuePress documentation on the actual implementation here: https://vuepress.vuejs.org/guide/.
198+
```sh
199+
npm run dev
200+
```
201+
202+
Once setup is complete, you can refer to VuePress documentation on the actual implementation here: <https://vuepress.vuejs.org/guide/>.
110203

111-
### Git/GitHub workflow
204+
## Git/GitHub workflow
112205

113206
This is our preferred process for opening a PR on GitHub:
114207

Cargo.lock

-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+8-6
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ tls-vendored = ["native-tls/vendored"]
3131
clap = "2.33.1"
3232
ansi_term = "0.12.1"
3333
dirs-next = "1.0.1"
34-
git2 = { version = "0.13.8", default-features = false, features = [] }
34+
git2 = { version = "0.13.8", default-features = false }
3535
toml = { version = "0.5.6", features = ["preserve_order"] }
3636
serde_json = "1.0.57"
3737
rayon = "1.3.1"
@@ -64,17 +64,19 @@ attohttpc = { version = "0.15.0", optional = true, default-features = false, fea
6464
native-tls = { version = "0.2", optional = true }
6565

6666
[target.'cfg(windows)'.dependencies]
67-
winapi = { version = "0.3", features = ["winuser", "securitybaseapi", "processthreadsapi", "handleapi", "impl-default"]}
67+
winapi = { version = "0.3", features = [
68+
"winuser",
69+
"securitybaseapi",
70+
"processthreadsapi",
71+
"handleapi",
72+
"impl-default",
73+
] }
6874

6975
[target.'cfg(not(windows))'.dependencies]
7076
nix = "0.18.0"
7177

7278
[dev-dependencies]
7379
tempfile = "3.1.0"
74-
# More realiable than std::fs version on Windows
75-
# For removing temporary directories manually when needed
76-
# This is what tempfile uses to delete temporary directories
77-
remove_dir_all = "0.5.3"
7880

7981
[profile.release]
8082
codegen-units = 1

src/bug_report.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::utils::exec_cmd;
22

3+
use clap::crate_version;
34
use std::fs;
45
use std::path::PathBuf;
56

@@ -254,5 +255,6 @@ mod tests {
254255

255256
let config_path = get_config_path("bash");
256257
assert_eq!("/test/home/.bashrc", config_path.unwrap().to_str().unwrap());
258+
env::remove_var("HOME");
257259
}
258260
}

src/context.rs

+26-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use git2::{ErrorCode::UnbornBranch, Repository, RepositoryState};
77
use once_cell::sync::OnceCell;
88
use std::collections::{HashMap, HashSet};
99
use std::env;
10+
use std::ffi::OsString;
1011
use std::fs;
1112
use std::path::{Path, PathBuf};
1213
use std::string::String;
@@ -33,6 +34,9 @@ pub struct Context<'a> {
3334

3435
/// The shell the user is assumed to be running
3536
pub shell: Shell,
37+
38+
/// A HashMap of environment variable mocks
39+
pub env: HashMap<&'a str, String>,
3640
}
3741

3842
impl<'a> Context<'a> {
@@ -82,11 +86,30 @@ impl<'a> Context<'a> {
8286
dir_contents: OnceCell::new(),
8387
repo: OnceCell::new(),
8488
shell,
89+
env: HashMap::new(),
90+
}
91+
}
92+
93+
// Retrives a environment variable from the os or from a table if in testing mode
94+
pub fn get_env<K: AsRef<str>>(&self, key: K) -> Option<String> {
95+
if cfg!(test) {
96+
self.env.get(key.as_ref()).map(|val| val.to_string())
97+
} else {
98+
env::var(key.as_ref()).ok()
99+
}
100+
}
101+
102+
// Retrives a environment variable from the os or from a table if in testing mode (os version)
103+
pub fn get_env_os<K: AsRef<str>>(&self, key: K) -> Option<OsString> {
104+
if cfg!(test) {
105+
self.env.get(key.as_ref()).map(OsString::from)
106+
} else {
107+
env::var_os(key.as_ref())
85108
}
86109
}
87110

88111
/// Convert a `~` in a path to the home directory
89-
fn expand_tilde(dir: PathBuf) -> PathBuf {
112+
pub fn expand_tilde(dir: PathBuf) -> PathBuf {
90113
if dir.starts_with("~") {
91114
let without_home = dir.strip_prefix("~").unwrap();
92115
return dirs_next::home_dir().unwrap().join(without_home);
@@ -165,7 +188,7 @@ impl<'a> Context<'a> {
165188
}
166189

167190
fn get_shell() -> Shell {
168-
let shell = std::env::var("STARSHIP_SHELL").unwrap_or_default();
191+
let shell = env::var("STARSHIP_SHELL").unwrap_or_default();
169192
match shell.as_str() {
170193
"bash" => Shell::Bash,
171194
"fish" => Shell::Fish,
@@ -310,7 +333,7 @@ impl<'a> ScanDir<'a> {
310333
self
311334
}
312335

313-
/// based on the current Pathbuf check to see
336+
/// based on the current PathBuf check to see
314337
/// if any of this criteria match or exist and returning a boolean
315338
pub fn is_match(&self) -> bool {
316339
self.dir_contents.has_any_extension(self.extensions)

src/formatter/parser.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use pest::{error::Error, iterators::Pair, Parser};
2+
use pest_derive::*;
23

34
use super::model::*;
45

src/lib.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
#[macro_use]
2-
extern crate pest_derive;
3-
41
// Lib is present to allow for benchmarking
52
pub mod config;
63
pub mod configs;
@@ -11,3 +8,6 @@ pub mod modules;
118
pub mod print;
129
pub mod segment;
1310
mod utils;
11+
12+
#[cfg(test)]
13+
mod test;

src/main.rs

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1+
use clap::{crate_authors, crate_version};
12
use std::io;
23
use std::time::SystemTime;
34

4-
#[macro_use]
5-
extern crate clap;
6-
#[macro_use]
7-
extern crate pest_derive;
8-
95
mod bug_report;
106
mod config;
117
mod configs;
@@ -19,6 +15,9 @@ mod print;
1915
mod segment;
2016
mod utils;
2117

18+
#[cfg(test)]
19+
mod test;
20+
2221
use crate::module::ALL_MODULES;
2322
use clap::{App, AppSettings, Arg, Shell, SubCommand};
2423

0 commit comments

Comments
 (0)