1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
//! Shared test-only helpers.
//!
//! These utilities are consumed by unit tests across the crate and must
//! stay in sync between shim-writing sites — see issue #642.
#![allow(clippy::unwrap_used, clippy::expect_used)]
pub(crate) mod failing_io {
//! Writer fixture that always returns `ErrorKind::Other` from
//! `write` and `flush`. Used to drive `?`-propagation Err branches
//! in destructive-command tests where the prompt/preview write or
//! the post-API-success writeln is expected to fail.
pub(crate) struct FailingWriter;
impl std::io::Write for FailingWriter {
fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::other("simulated write failure"))
}
fn flush(&mut self) -> std::io::Result<()> {
Err(std::io::Error::other("simulated flush failure"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
/// Direct cover for `FailingWriter::flush`. The destructive-command
/// tests fail at the prior `write!` so flush never fires; this
/// asserts its body still returns the expected error.
#[test]
fn flush_returns_error() {
let mut w = FailingWriter;
let err = w.flush().unwrap_err();
assert!(err.to_string().contains("simulated flush failure"));
}
}
}
pub(crate) mod env {
//! Pure in-memory [`EnvSource`](crate::utils::env::EnvSource) for tests.
//!
//! `MapEnv` lets env-parsing boundaries be tested without mutating the
//! process-global environment: a test builds its own map and passes
//! `&map` to the seam's `*_with(&impl EnvSource, …)` entry point. Because
//! the map is an owned value with no shared state, such tests need no
//! lock and run fully in parallel (issue #1030 / #821).
use std::collections::HashMap;
/// An [`EnvSource`](crate::utils::env::EnvSource) backed by an in-memory
/// map — the test counterpart to
/// [`SystemEnv`](crate::utils::env::SystemEnv).
#[derive(Debug, Default, Clone)]
pub(crate) struct MapEnv(HashMap<String, String>);
impl MapEnv {
/// Creates an empty environment (every lookup returns `None`).
pub(crate) fn new() -> Self {
Self::default()
}
/// Inserts `key = value` and returns `self`, for builder-style setup.
pub(crate) fn with(mut self, key: &str, value: &str) -> Self {
self.0.insert(key.to_string(), value.to_string());
self
}
}
impl crate::utils::env::EnvSource for MapEnv {
fn var(&self, key: &str) -> Option<String> {
self.0.get(key).cloned()
}
}
}
pub(crate) mod atlassian_env {
//! In-process Atlassian env-var guard for tests that drive
//! `cli::atlassian::helpers::create_client()` end-to-end.
//!
//! Mirrors `tests/mcp_integration_test.rs::AtlassianEnvGuard` (which lives
//! in a *separate* integration-test process and so keeps its own lock).
//! Within the lib-test process every guard that mutates the Atlassian
//! credential env vars **must serialise on the one canonical mutex**
//! [`crate::atlassian::auth::test_util::AUTH_ENV_MUTEX`] — independent
//! mutexes over the same process-global vars provide no mutual exclusion
//! and caused the flaky env race in issue #950.
//!
//! This is transitional scaffolding: as the remaining `*Command` tests
//! migrate to the [`create_client_from`] dependency-injection seam (and
//! stop mutating env entirely), their use of this guard — and eventually
//! the guard itself — can be removed.
//!
//! [`create_client_from`]: crate::cli::atlassian::helpers::create_client_from
use std::sync::MutexGuard;
pub(crate) struct AtlassianEnvGuard {
_guard: MutexGuard<'static, ()>,
prev_home: Option<String>,
prev_xdg: Option<String>,
prev_url: Option<String>,
prev_email: Option<String>,
prev_token: Option<String>,
_tmp: tempfile::TempDir,
}
impl AtlassianEnvGuard {
/// Repoints HOME at an empty tempdir and sets the Atlassian env
/// vars so `create_client()` produces a client targeting the
/// supplied URL with the supplied credentials.
pub(crate) fn new(instance_url: &str, email: &str, token: &str) -> Self {
let guard = crate::atlassian::auth::test_util::AUTH_ENV_MUTEX
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let tmp = tempfile::tempdir().unwrap();
let prev_home = std::env::var("HOME").ok();
let prev_xdg = std::env::var("XDG_CONFIG_HOME").ok();
let prev_url = std::env::var("ATLASSIAN_INSTANCE_URL").ok();
let prev_email = std::env::var("ATLASSIAN_EMAIL").ok();
let prev_token = std::env::var("ATLASSIAN_API_TOKEN").ok();
std::env::set_var("HOME", tmp.path());
std::env::set_var("XDG_CONFIG_HOME", tmp.path().join("xdg"));
std::env::set_var("ATLASSIAN_INSTANCE_URL", instance_url);
std::env::set_var("ATLASSIAN_EMAIL", email);
std::env::set_var("ATLASSIAN_API_TOKEN", token);
Self {
_guard: guard,
prev_home,
prev_xdg,
prev_url,
prev_email,
prev_token,
_tmp: tmp,
}
}
}
impl Drop for AtlassianEnvGuard {
fn drop(&mut self) {
restore("HOME", self.prev_home.as_deref());
restore("XDG_CONFIG_HOME", self.prev_xdg.as_deref());
restore("ATLASSIAN_INSTANCE_URL", self.prev_url.as_deref());
restore("ATLASSIAN_EMAIL", self.prev_email.as_deref());
restore("ATLASSIAN_API_TOKEN", self.prev_token.as_deref());
}
}
fn restore(key: &str, prev: Option<&str>) {
match prev {
Some(v) => std::env::set_var(key, v),
None => std::env::remove_var(key),
}
}
}
#[cfg(unix)]
pub(crate) mod shim {
use std::path::Path;
use std::sync::{Mutex, MutexGuard};
/// Serializes every test that writes an executable shim and then
/// `execve`s it. Belt-and-braces pairing with `write_exec_script`'s
/// sync+close: even with each test's FD fully released before exec,
/// high parallelism (cargo llvm-cov) could still land a `fork()` from
/// one test while another thread's writable FD was live, letting the
/// child inherit it and hit `ETXTBSY`. See issue #642.
static SHIM_LOCK: Mutex<()> = Mutex::new(());
/// Acquires the crate-wide shim lock, recovering from poisoning so
/// intentional panics in one test don't cascade into the rest of the
/// suite.
pub(crate) fn shim_lock() -> MutexGuard<'static, ()> {
SHIM_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
/// Writes an executable script at `path`, flushes it to disk, and
/// explicitly drops the writable FD before returning. Setting mode
/// via `OpenOptions` avoids a second open-for-write that
/// `chmod`-after-`fs::write` would cause.
pub(crate) fn write_exec_script(path: &Path, script: &str) {
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o755)
.open(path)
.unwrap();
file.write_all(script.as_bytes()).unwrap();
file.sync_all().unwrap();
drop(file);
}
}