Some refacto on RunContext to better manage color/no_color.

This commit is contained in:
Jean-Christophe Amiel 2025-11-10 14:24:18 +01:00 committed by hurl-bot
parent 1e96255d2c
commit 39986a9be2
No known key found for this signature in database
GPG Key ID: 1283A2B4A0DCAF8D
4 changed files with 113 additions and 90 deletions

View File

@ -4,7 +4,8 @@ set -Eeuo pipefail
valgrind --version
cargo-valgrind --help
cat <<END | cargo valgrind run -p hurl -- --test
GET https://unpkg.com/vue@3.4.27/dist/vue.global.prod.js
HTTP 200
END
# Disable valgrind for the moment, see <https://github.com/jfrimmel/cargo-valgrind/issues/131>
#cat <<END | cargo valgrind run -p hurl -- --test
#GET https://unpkg.com/vue@3.4.27/dist/vue.global.prod.js
#HTTP 200
#END

View File

@ -15,15 +15,18 @@
* limitations under the License.
*
*/
use std::collections::HashMap;
/// Represents the context in whin is executed Hurl: the env variables, whether standard
/// Represents the context in which is executed Hurl: the env variables, whether standard
/// input is a terminal or not (when pipe or redirected to a file for instance), whether standard
/// error is a terminal or not, whether Hurl is executed in a CI/CD environment, whether users has
/// disallowed ANSI code color etc...
pub struct RunContext {
/// Are we allowed to ise ANSI escaoe codes or not.
with_color: bool,
env_vars: Vec<(String, String)>,
/// Whether this running in a Continuous Integration environment.
/// All the environment variables.
env_vars: HashMap<String, String>,
/// Whether we're running in a Continuous Integration environment or not.
ci: bool,
/// Is standard input a terminal or not?
stdin_term: bool,
@ -41,16 +44,26 @@ impl RunContext {
/// Creates a new context. The environment is captured and will be seen as non-mutable for the
/// execution with this context.
pub fn new(
with_color: bool,
env_vars: Vec<(String, String)>,
env_vars: HashMap<String, String>,
stdin_term: bool,
stdout_term: bool,
stderr_term: bool,
) -> Self {
// Code borrowed from <https://github.com/rust-lang/cargo/blob/master/crates/cargo-util/src/lib.rs>
let ci = env_vars
.iter()
.any(|(name, _)| name == "CI" || name == "TF_BUILD");
let ci = env_vars.contains_key("CI") || env_vars.contains_key("TF_BUILD");
// According to the NO_COLOR spec, any presence of the variable should disable color, but to
// maintain backward compatibility with code < 7.1.0, we check that the NO_COLOR env is at
// least not empty.
let with_color = if let Some(v) = env_vars.get("NO_COLOR") {
if !v.is_empty() {
false
} else {
stdout_term
}
} else {
stdout_term
};
RunContext {
with_color,
@ -67,11 +80,11 @@ impl RunContext {
self.with_color
}
/// Returns the list of Hurl variables injected by environment variables.
/// Returns the map of Hurl variables injected by environment variables.
///
/// Environment variables are prefixed with `HURL_VARIABLE_` and returned values have their name
/// stripped of this prefix.
pub fn var_env_vars(&self) -> Vec<(&str, &str)> {
pub fn var_env_vars(&self) -> HashMap<&str, &str> {
self.env_vars
.iter()
.filter_map(|(name, value)| {
@ -82,11 +95,11 @@ impl RunContext {
.collect()
}
/// Returns the list of legacy Hurl variables injected by environment variables.
/// Returns the map of legacy Hurl variables injected by environment variables.
///
/// Environment variables are prefixed with `HURL_` and returned values have their name
/// stripped of this prefix.
pub fn legacy_var_env_vars(&self) -> Vec<(&str, &str)> {
pub fn legacy_var_env_vars(&self) -> HashMap<&str, &str> {
self.env_vars
.iter()
.filter_map(|(name, value)| {
@ -101,11 +114,11 @@ impl RunContext {
.collect()
}
/// Returns the list of Hurl secrets injected by environment variables.
/// Returns the map of Hurl secrets injected by environment variables.
///
/// Environment variables are prefixed with `HURL_SECRET_` and returned values have their name
/// stripped of this prefix.
pub fn secret_env_vars(&self) -> Vec<(&str, &str)> {
pub fn secret_env_vars(&self) -> HashMap<&str, &str> {
self.env_vars
.iter()
.filter_map(|(name, value)| {
@ -141,35 +154,58 @@ impl RunContext {
#[cfg(test)]
mod tests {
use crate::cli::options::context::RunContext;
use std::collections::HashMap;
#[test]
fn empty_variables_secrets_from_env() {
let with_color = false;
fn context_is_colored() {
let stdin_term = true;
let stdout_term = true;
let stderr_term = true;
let env_vars = vec![
let env_vars = HashMap::from([("A".to_string(), "B".to_string())]);
let ctx = RunContext::new(env_vars, stdin_term, stdout_term, stderr_term);
assert!(ctx.is_with_color());
}
#[test]
fn context_respect_no_color() {
let stdin_term = true;
let stdout_term = true;
let stderr_term = true;
let env_vars = HashMap::from([("NO_COLOR".to_string(), "1".to_string())]);
let ctx = RunContext::new(env_vars, stdin_term, stdout_term, stderr_term);
assert!(!ctx.is_with_color());
}
#[test]
fn empty_variables_secrets_from_env() {
let stdin_term = true;
let stdout_term = true;
let stderr_term = true;
let env_vars = HashMap::from([
("FOO".to_string(), "xxx".to_string()),
("BAR".to_string(), "yyy".to_string()),
("BAZ".to_string(), "yyy".to_string()),
];
]);
let ctx = RunContext::new(with_color, env_vars, stdin_term, stdout_term, stderr_term);
let ctx = RunContext::new(env_vars, stdin_term, stdout_term, stderr_term);
assert_eq!(ctx.var_env_vars(), vec![]);
assert_eq!(ctx.legacy_var_env_vars(), vec![]);
assert_eq!(ctx.secret_env_vars(), vec![]);
assert!(ctx.var_env_vars().is_empty());
assert!(ctx.legacy_var_env_vars().is_empty());
assert!(ctx.secret_env_vars().is_empty());
}
#[test]
fn variables_from_env() {
let with_color = false;
let stdin_term = true;
let stdout_term = true;
let stderr_term = true;
let env_vars = vec![
let env_vars = HashMap::from([
("FOO".to_string(), "xxx".to_string()),
("BAR".to_string(), "yyy".to_string()),
("BAZ".to_string(), "yyy".to_string()),
@ -179,26 +215,26 @@ mod tests {
("HURL_VARIABLE".to_string(), "1234".to_string()),
("HURL_VARIABLE_".to_string(), "abcd".to_string()),
("HURL_VARIABLE_FOO".to_string(), "def".to_string()),
];
]);
let ctx = RunContext::new(with_color, env_vars, stdin_term, stdout_term, stderr_term);
let ctx = RunContext::new(env_vars, stdin_term, stdout_term, stderr_term);
assert_eq!(
ctx.var_env_vars(),
vec![("foo", "true"), ("id", "1234"), ("FOO", "def"),]
);
assert_eq!(ctx.legacy_var_env_vars(), vec![("VARIABLE", "1234"),]);
assert_eq!(ctx.secret_env_vars(), vec![]);
assert_eq!(ctx.var_env_vars().len(), 3);
assert_eq!(ctx.var_env_vars()["foo"], "true");
assert_eq!(ctx.var_env_vars()["id"], "1234");
assert_eq!(ctx.var_env_vars()["FOO"], "def");
assert_eq!(ctx.legacy_var_env_vars().len(), 1);
assert_eq!(ctx.legacy_var_env_vars()["VARIABLE"], "1234");
assert!(ctx.secret_env_vars().is_empty());
}
#[test]
fn legacy_variables_from_env() {
let with_color = false;
let stdin_term = true;
let stdout_term = true;
let stderr_term = true;
let env_vars = vec![
let env_vars = HashMap::from([
("FOO".to_string(), "xxx".to_string()),
("BAR".to_string(), "yyy".to_string()),
("BAZ".to_string(), "yyy".to_string()),
@ -209,41 +245,41 @@ mod tests {
("HURL_".to_string(), "1234".to_string()),
("HURL_".to_string(), "abcd".to_string()),
("HURL_FOO".to_string(), "def".to_string()),
];
]);
let ctx = RunContext::new(with_color, env_vars, stdin_term, stdout_term, stderr_term);
let ctx = RunContext::new(env_vars, stdin_term, stdout_term, stderr_term);
assert_eq!(ctx.var_env_vars(), vec![("bar", "def"),]);
assert_eq!(
ctx.legacy_var_env_vars(),
vec![("foo", "true"), ("id", "1234"), ("FOO", "def"),]
);
assert_eq!(ctx.secret_env_vars(), vec![]);
assert_eq!(ctx.var_env_vars().len(), 1);
assert_eq!(ctx.var_env_vars()["bar"], "def");
assert_eq!(ctx.legacy_var_env_vars().len(), 3);
assert_eq!(ctx.legacy_var_env_vars()["foo"], "true");
assert_eq!(ctx.legacy_var_env_vars()["id"], "1234");
assert_eq!(ctx.legacy_var_env_vars()["FOO"], "def");
assert!(ctx.secret_env_vars().is_empty());
}
#[test]
fn legacy_secrets_from_env() {
let with_color = false;
let stdin_term = true;
let stdout_term = true;
let stderr_term = true;
let env_vars = vec![
let env_vars = HashMap::from([
("FOO".to_string(), "xxx".to_string()),
("HURL_SECRET".to_string(), "48".to_string()),
("HURL_SECRET_".to_string(), "48".to_string()),
("HURL_SECRET_abcd".to_string(), "1234".to_string()),
("HURL_SECRET_ABCD".to_string(), "5678".to_string()),
("BAR".to_string(), "bar".to_string()),
];
]);
let ctx = RunContext::new(with_color, env_vars, stdin_term, stdout_term, stderr_term);
let ctx = RunContext::new(env_vars, stdin_term, stdout_term, stderr_term);
assert_eq!(ctx.var_env_vars(), vec![]);
assert_eq!(ctx.legacy_var_env_vars(), vec![("SECRET", "48"),]);
assert_eq!(
ctx.secret_env_vars(),
vec![("abcd", "1234"), ("ABCD", "5678"),]
);
assert!(ctx.var_env_vars().is_empty());
assert_eq!(ctx.legacy_var_env_vars().len(), 1);
assert_eq!(ctx.legacy_var_env_vars()["SECRET"], "48");
assert_eq!(ctx.secret_env_vars().len(), 2);
assert_eq!(ctx.secret_env_vars()["abcd"], "1234");
assert_eq!(ctx.secret_env_vars()["ABCD"], "5678");
}
}

View File

@ -24,10 +24,9 @@ mod variables;
mod variables_file;
use std::collections::HashMap;
use std::io::IsTerminal;
use std::env;
use std::path::{Path, PathBuf};
use std::time::Duration;
use std::{env, io};
use clap::builder::styling::{AnsiColor, Effects};
use clap::builder::Styles;
@ -44,7 +43,7 @@ use hurl_core::input::{Input, InputKind};
use hurl_core::types::{BytesPerSec, Count};
use crate::cli;
use crate::cli::options::context::RunContext;
pub use crate::cli::options::context::RunContext;
use crate::runner::{RunnerOptions, RunnerOptionsBuilder, Value};
/// Represents the list of all options that can be used in Hurl command line.
@ -179,11 +178,9 @@ fn get_version() -> String {
)
}
/// Parse the Hurl CLI options and returns a [`CliOptions`] result.
///
/// When a [`CliOptionsError::DisplayHelp`] variant is returned, `with_color` is used
/// to print an ANSI color help or not.
pub fn parse(with_color: bool) -> Result<CliOptions, CliOptionsError> {
/// Parse the Hurl CLI options and returns a [`CliOptions`] result, given a run `context`
/// (environment variables).
pub fn parse(context: &RunContext) -> Result<CliOptions, CliOptionsError> {
let styles = Styles::styled()
.header(AnsiColor::Green.on_default() | Effects::BOLD)
.usage(AnsiColor::Green.on_default() | Effects::BOLD)
@ -277,20 +274,13 @@ pub fn parse(with_color: bool) -> Result<CliOptions, CliOptionsError> {
let arg_matches = command.try_get_matches_from_mut(env::args_os());
let arg_matches = match arg_matches {
Ok(args) => args,
Err(error) => return Err(CliOptionsError::from_clap(error, with_color)),
Err(error) => return Err(CliOptionsError::from_clap(error, context.is_with_color())),
};
// Construct the run context environment
let env_vars = env::vars().collect();
let stdin_term = io::stdin().is_terminal();
let stdout_term = io::stdout().is_terminal();
let stderr_term = io::stderr().is_terminal();
let ctx = RunContext::new(with_color, env_vars, stdin_term, stdout_term, stderr_term);
// If we've no file input (either from the standard input or from the command line arguments),
// we just print help and exit.
if !matches::has_input_files(&arg_matches, &ctx) {
let help = if with_color {
if !matches::has_input_files(&arg_matches, context) {
let help = if context.is_with_color() {
command.render_help().ansi().to_string()
} else {
command.render_help().to_string()
@ -298,7 +288,7 @@ pub fn parse(with_color: bool) -> Result<CliOptions, CliOptionsError> {
return Err(CliOptionsError::NoInput(help));
}
let opts = parse_matches(&arg_matches, &ctx)?;
let opts = parse_matches(&arg_matches, context)?;
if opts.input_files.is_empty() {
return Err(CliOptionsError::Error(
"No input files provided".to_string(),

View File

@ -32,7 +32,7 @@ use hurl::util::redacted::Redact;
use hurl_core::input::Input;
use hurl_core::text;
use crate::cli::options::{CliOptions, CliOptionsError};
use crate::cli::options::{CliOptions, CliOptionsError, RunContext};
use crate::cli::{BaseLogger, CliError};
const EXIT_OK: i32 = 0;
@ -56,9 +56,17 @@ struct HurlRun {
fn main() {
text::init_crate_colored();
let with_color = is_color_allowed_from_env();
// Construct the run context environment, this should be the sole place where we read
// environment variables. The run context will be injected in functions that need to access
// environment variables.
// TODO: add `env::current_dir` to the run context
let env_vars = env::vars().collect();
let stdin_term = io::stdin().is_terminal();
let stdout_term = io::stdout().is_terminal();
let stderr_term = io::stderr().is_terminal();
let ctx = RunContext::new(env_vars, stdin_term, stdout_term, stderr_term);
let opts = match cli::options::parse(with_color) {
let opts = match cli::options::parse(&ctx) {
Ok(v) => v,
Err(e) => match e {
CliOptionsError::DisplayHelp(e) | CliOptionsError::DisplayVersion(e) => {
@ -155,18 +163,6 @@ fn has_report(opts: &CliOptions) -> bool {
|| opts.cookie_output_file.is_some()
}
/// Returns `true` if we can use ANSI color, solely base on the environment.
///
/// This function doesn't take CLI options into account.
fn is_color_allowed_from_env() -> bool {
if let Ok(v) = env::var("NO_COLOR") {
if !v.is_empty() {
return false;
}
}
io::stdout().is_terminal()
}
/// Writes `runs` results on file, in HTML, TAP, JUnit or Cookie file format.
fn export_results(
runs: &[HurlRun],