Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions e2e/cli/test_error_display
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/usr/bin/env bash

set -euo pipefail

# Test comprehensive error display
# This test validates that error messages are properly formatted and structured

# Try to disable colors
export NO_COLOR=1

# Local assert_fail function that doesn't force MISE_FRIENDLY_ERROR
local_assert_fail() {
local cmd="$1"
local expected="${2:-}"
local status=0
local actual

debug "$ $cmd"
actual=$(RUST_BACKTRACE=0 bash -c "$cmd 2>&1") || status=$?

if [[ $status -eq 0 ]]; then
fail "[$cmd] command succeeded but was expected to fail"
fi

if [[ -z $expected ]]; then
ok "[$cmd] expected failure"
elif [[ $actual == *"$expected"* ]]; then
ok "[$cmd] output contains expected text"
else
fail "[$cmd] expected '$expected' but got '$actual'"
fi
}

echo "Testing error message formatting..."

# =============================================================================
# PART 1: Testing with FRIENDLY errors (simplified output)
# =============================================================================
echo ""
echo "PART 1: Testing FRIENDLY error format (MISE_FRIENDLY_ERROR=1)"
echo "============================================================"
export MISE_FRIENDLY_ERROR=1

# Test 1: Invalid tool version error
echo "Test 1: Invalid tool version"
local_assert_fail "mise install core:node@invalid-version" \
"mise ERROR Failed to install core:node@invalid-version:
0: HTTP status client error (404 Not Found) for url (https://nodejs.org/dist/vinvalid-version/node-vinvalid-version.tar.gz)"

# Test 2: Backend error (cargo)
echo "Test 2: Cargo backend error"
local_assert_fail "mise install cargo:nonexistent-crate-12345@1.0.0" \
"mise ERROR Failed to install cargo:nonexistent-crate-12345@1.0.0:
0: HTTP status client error (404 Not Found) for url (https://index.crates.io/no/ne/nonexistent-crate-12345)"

# Test 3: GitHub repository not found
echo "Test 3: GitHub repository not found"
local_assert_fail "mise install github:nonexistent-org/nonexistent-repo@latest" \
"mise ERROR Failed to install github:nonexistent-org/nonexistent-repo@latest:
0: HTTP status client error (404 Not Found) for url (https://api.github.com/repos/nonexistent-org/nonexistent-repo/releases)"

# Test 4: Plugin not found
echo "Test 4: Plugin not found"
local_assert_fail "mise install nonexistent-tool@1.0.0" \
"mise ERROR nonexistent-tool not found in mise tool registry"

# Test 5: Multiple tool failures (could be single or multiple depending on timing)
echo "Test 5: Multiple tool failures"
local_assert_fail "mise install tiny@999.999.999 jq@999.999.999" \
"mise ERROR Failed to install aqua:jqlang/jq@999.999.999:
0: HTTP status client error (404 Not Found) for url (https://api.github.com/repos/jqlang/jq/releases/tags/jq-999.999.999)"

# =============================================================================
# PART 2: Testing with DETAILED errors (full error chains)
# =============================================================================
echo ""
echo "PART 2: Testing DETAILED error format (MISE_FRIENDLY_ERROR=0)"
echo "============================================================"
export MISE_FRIENDLY_ERROR=0

# Test 1: Invalid tool version error - check for the numbered error format with more context
echo "Test 1: Invalid tool version"
local_assert_fail "mise install core:node@invalid-version" \
"Error:
0: Failed to install core:node@invalid-version:
0: HTTP status client error (404 Not Found) for url (https://nodejs.org/dist/vinvalid-version/node-vinvalid-version.tar.gz)"

# Test 2: Backend error (cargo) - check for numbered error format with more context
echo "Test 2: Cargo backend error"
local_assert_fail "mise install cargo:nonexistent-crate-12345@1.0.0" \
"Error:
0: Failed to install cargo:nonexistent-crate-12345@1.0.0:
0: HTTP status client error (404 Not Found) for url (https://index.crates.io/no/ne/nonexistent-crate-12345)"

# Test 3: GitHub repository not found - check for numbered error format with more context
echo "Test 3: GitHub repository not found"
local_assert_fail "mise install github:nonexistent-org/nonexistent-repo@latest" \
"Error:
0: Failed to install github:nonexistent-org/nonexistent-repo@latest:
0: HTTP status client error (404 Not Found) for url (https://api.github.com/repos/nonexistent-org/nonexistent-repo/releases)"

# Test 4: Multiple tool failures - check for numbered error format
echo "Test 4: Multiple tool failures"
local_assert_fail "mise install tiny@999.999.999 jq@999.999.999" \
"Error:
0: Failed to install aqua:jqlang/jq@999.999.999:
0: HTTP status client error (404 Not Found) for url (https://api.github.com/repos/jqlang/jq/releases/tags/jq-999.999.999)"

# Test 5: Error with backtrace enabled - check for proper backtrace format
echo "Test 5: Error with backtrace"
local_assert_fail "RUST_BACKTRACE=1 mise install core:node@invalid-version" \
"Error:
0: Failed to install core:node@invalid-version: HTTP status client error (404 Not Found) for url (https://nodejs.org/dist/vinvalid-version/node-vinvalid-version.tar.gz)"

# Test 6: Invalid configuration (treated as version strings)
echo "Test 6: Invalid configuration"
cat >test_invalid_config.toml <<EOF
[tools]
node = "this is not valid"
python = ["also", "not", "valid"]
EOF

local_assert_fail "MISE_CONFIG_FILE=test_invalid_config.toml mise install" \
"Error:
0: Failed to install tools: core:python@also, core:python@not, core:python@valid, core:node@this is not valid"
rm -f test_invalid_config.toml

echo ""
echo "All error display tests passed!"
4 changes: 2 additions & 2 deletions e2e/cli/test_install_parallel_failure
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ EOF

# Try to install both tools in parallel
# The dummy tool should fail, but tiny should succeed
assert_fail "mise install" "Failed to install tool: dummy@other-dummy"
assert_fail "mise install" "Failed to install asdf:dummy@other-dummy"

# Verify tiny was installed successfully despite dummy failure
assert_contains "mise ls --installed tiny" "3.1.0"
Expand All @@ -45,7 +45,7 @@ mise uninstall jq --all 2>/dev/null || true
mise uninstall tiny --all 2>/dev/null || true

# Create a scenario with one valid and one invalid tool
assert_fail "mise install tiny@latest jq@999.999.999" "Failed to install tool: jq@999.999.999"
assert_fail "mise install tiny@latest jq@999.999.999" "Failed to install aqua:jqlang/jq@999.999.999"

# Verify the valid tool was installed
assert_contains "mise ls --installed tiny" "3.1.0"
Expand Down
2 changes: 1 addition & 1 deletion e2e/cli/test_upgrade_parallel_failure
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ echo "1.0.0" >"$MISE_DATA_DIR/installs/tiny/1.0.0/version"
# Now `mise up` should try to install/upgrade both:
# - dummy: install other-dummy (will fail)
# - tiny: upgrade to latest 3.1.0 (will succeed)
assert_fail "mise up" "Failed to install tool: dummy@other-dummy"
assert_fail "mise up" "Failed to install asdf:dummy@other-dummy"

# Verify tiny was upgraded successfully despite dummy failure
assert_contains "mise ls --installed tiny" "3.1.0"
Expand Down
1 change: 1 addition & 0 deletions e2e/tasks/test_task_parallel_execution
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ assert "mise install"

echo "Testing parallel task execution (running 3 times for reliability)..."

# Run the test 3 times to ensure reliability
for test_run in {1..3}; do
echo ""
echo "=== Test Run $test_run/3 ==="
Expand Down
10 changes: 10 additions & 0 deletions mise.lock
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ checksum = "blake3:e96726cf26e534a44f61e015cc427ed92868fbf351fb7f1245d30e044d74c
size = 6774865
url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.15.4/cargo-binstall-x86_64-unknown-linux-musl.tgz"

[tools.cargo-binstall.platforms.macos-arm64]
checksum = "blake3:6a9104a686d44217ccfdcc2b9333a708d8ebd5a4abafe4d2eda5dec77c051a67"
size = 5997376
url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.15.4/cargo-binstall-aarch64-apple-darwin.zip"

[[tools."cargo:cargo-edit"]]
version = "0.13.7"
backend = "cargo:cargo-edit"
Expand Down Expand Up @@ -108,6 +113,11 @@ checksum = "blake3:5f4f62704bca96314d3ae11e2b0f0c9e76d07058f18676a5c31981763eb81
size = 6753765
url = "https://github.com/jdx/hk/releases/download/v1.12.0/hk-x86_64-unknown-linux-gnu.tar.gz"

[tools.hk.platforms.macos-arm64]
checksum = "blake3:4a5e2f34cd73cc54e9beafb189f1566c9d2a3de26eb6aae1a3ee69312bd752ac"
size = 5853150
url = "https://github.com/jdx/hk/releases/download/v1.12.0/hk-aarch64-apple-darwin.tar.gz"

[[tools.jq]]
version = "1.8.1"
backend = "aqua:jqlang/jq"
Expand Down
1 change: 1 addition & 0 deletions src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,7 @@ pub trait Backend: Debug + Send + Sync {
Ok(tv) => tv,
Err(e) => {
self.cleanup_install_dirs_on_error(&old_tv);
// Pass through the error - it will be wrapped at a higher level
return Err(e);
}
};
Expand Down
4 changes: 4 additions & 0 deletions src/config/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ impl Settings {
if settings.raw {
settings.jobs = 1;
}
// Handle NO_COLOR environment variable
if *env::NO_COLOR {
settings.color = false;
}
if settings.debug {
settings.log_level = "debug".to_string();
}
Expand Down
18 changes: 16 additions & 2 deletions src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,17 @@ pub static XDG_DATA_HOME: Lazy<PathBuf> = Lazy::new(|| {
pub static XDG_STATE_HOME: Lazy<PathBuf> =
Lazy::new(|| var_path("XDG_STATE_HOME").unwrap_or_else(|| HOME.join(".local").join("state")));

/// always display "friendly" errors even in debug mode
pub static MISE_FRIENDLY_ERROR: Lazy<bool> = Lazy::new(|| var_is_true("MISE_FRIENDLY_ERROR"));
/// control display of "friendly" errors - defaults to release mode behavior unless overridden
pub static MISE_FRIENDLY_ERROR: Lazy<bool> = Lazy::new(|| {
if var_is_true("MISE_FRIENDLY_ERROR") {
true
} else if var_is_false("MISE_FRIENDLY_ERROR") {
false
} else {
// default behavior: friendly in release mode unless debug logging
!cfg!(debug_assertions) && log::max_level() < log::LevelFilter::Debug
}
});
pub static MISE_TOOL_STUB: Lazy<bool> =
Lazy::new(|| ARGS.read().unwrap().get(1).map(|s| s.as_str()) == Some("tool-stub"));
pub static MISE_NO_CONFIG: Lazy<bool> = Lazy::new(|| var_is_true("MISE_NO_CONFIG"));
Expand Down Expand Up @@ -256,13 +265,18 @@ pub static CLICOLOR_FORCE: Lazy<Option<bool>> =
pub static CLICOLOR: Lazy<Option<bool>> = Lazy::new(|| {
if *CLICOLOR_FORCE == Some(true) {
Some(true)
} else if *NO_COLOR {
Some(false)
} else if let Ok(v) = var("CLICOLOR") {
Some(v != "0")
} else {
None
}
});

/// Disable color output - https://no-color.org/
pub static NO_COLOR: Lazy<bool> = Lazy::new(|| var("NO_COLOR").is_ok_and(|v| !v.is_empty()));

// python
pub static PYENV_ROOT: Lazy<PathBuf> =
Lazy::new(|| var_path("PYENV_ROOT").unwrap_or_else(|| HOME.join(".pyenv")));
Expand Down
33 changes: 25 additions & 8 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,32 @@ fn format_install_failures(failed_installations: &[(ToolRequest, Report)]) -> St
return "Installation failed".to_string();
}

// For a single failure, show the underlying error directly to preserve
// the original error location for better debugging
if failed_installations.len() == 1 {
let (tr, error) = &failed_installations[0];
// Show the underlying error with the tool context
return format!(
"Failed to install {}@{}: {}",
tr.ba().full(),
tr.version(),
if *RUST_BACKTRACE {
format!("{error}")
} else {
format!("{error:?}")
}
);
}

// For multiple failures, show a summary and then each error
let mut output = vec![];
let failed_tools: Vec<String> = failed_installations
.iter()
.map(|(tr, _)| format!("{}@{}", tr.ba().short, tr.version()))
.map(|(tr, _)| format!("{}@{}", tr.ba().full(), tr.version()))
.collect();

output.push(format!(
"Failed to install {}: {}",
if failed_tools.len() == 1 {
"tool"
} else {
"tools"
},
"Failed to install tools: {}",
failed_tools.join(", ")
));

Expand All @@ -69,7 +82,11 @@ fn format_install_failures(failed_installations: &[(ToolRequest, Report)]) -> St
} else {
format!("{error:?}")
};
output.push(format!("\n{}@{}: {error_str}", tr.ba().short, tr.version()));
output.push(format!(
"\n{}@{}: {error_str}",
tr.ba().full(),
tr.version()
));
}

output.join("\n")
Expand Down
15 changes: 11 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,16 @@ fn main() -> eyre::Result<()> {
}

async fn main_() -> eyre::Result<()> {
color_eyre::install()?;
// Configure color-eyre based on color preferences
if *env::CLICOLOR == Some(false) {
// Use blank theme (no colors) when colors are disabled
color_eyre::config::HookBuilder::new()
.theme(color_eyre::config::Theme::new())
.install()?;
} else {
// Use default installation with colors
color_eyre::install()?;
}
install_panic_hook();
if std::env::current_dir().is_ok() {
unsafe {
Expand Down Expand Up @@ -134,9 +143,7 @@ fn handle_err(err: Report) -> eyre::Result<()> {
}
}
show_github_rate_limit_err(&err);
if *env::MISE_FRIENDLY_ERROR
|| (!cfg!(debug_assertions) && log::max_level() < log::LevelFilter::Debug)
{
if *env::MISE_FRIENDLY_ERROR {
display_friendly_err(&err);
exit(1);
}
Expand Down
10 changes: 5 additions & 5 deletions src/toolset/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use crate::{backend::Backend, parallel};
pub use builder::ToolsetBuilder;
use console::truncate_str;
use dashmap::DashMap;
use eyre::{Result, WrapErr};
use eyre::Result;
use indexmap::{IndexMap, IndexSet};
use itertools::Itertools;
use outdated_info::OutdatedInfo;
Expand Down Expand Up @@ -255,6 +255,7 @@ impl Toolset {
// Count both successes and failures toward header progress
mpr.header_inc(successful_installations.len() + failed_installations.len());
installed.extend(successful_installations);

return Err(Error::InstallFailed {
successful_installations: installed,
failed_installations,
Expand Down Expand Up @@ -454,10 +455,9 @@ impl Toolset {
force: opts.force,
dry_run: opts.dry_run,
};
let old_tv = tv.clone();
ba.install_version(ctx, tv)
.await
.wrap_err_with(|| format!("failed to install {old_tv}"))
// Avoid wrapping the backend error here so the error location
// points to the backend implementation (more helpful for debugging).
ba.install_version(ctx, tv).await
}
.await;

Expand Down
Loading