|
1 | 1 | use std::path::Path; |
2 | 2 | use std::process::Command; |
3 | 3 |
|
| 4 | +#[cfg(unix)] |
| 5 | +use std::os::unix::fs::PermissionsExt; |
| 6 | + |
4 | 7 | use assert_fs::prelude::*; |
5 | 8 | use uv_static::EnvVars; |
6 | 9 |
|
@@ -10662,3 +10665,111 @@ fn build_isolation_override() -> anyhow::Result<()> { |
10662 | 10665 |
|
10663 | 10666 | Ok(()) |
10664 | 10667 | } |
| 10668 | + |
| 10669 | +/// Skip configuration in parent directory when permissions are denied. |
| 10670 | +#[test] |
| 10671 | +#[cfg_attr( |
| 10672 | + windows, |
| 10673 | + ignore = "Configuration tests are not yet supported on Windows" |
| 10674 | +)] |
| 10675 | +#[cfg(unix)] |
| 10676 | +fn resolve_permission_denied() -> anyhow::Result<()> { |
| 10677 | + // RAII guard to ensure permissions are restored even if the test fails or panics. |
| 10678 | + struct PermissionGuard<'a> { |
| 10679 | + path: &'a std::path::Path, |
| 10680 | + should_restore: bool, |
| 10681 | + } |
| 10682 | + |
| 10683 | + impl Drop for PermissionGuard<'_> { |
| 10684 | + fn drop(&mut self) { |
| 10685 | + if self.should_restore { |
| 10686 | + let _ = fs_err::set_permissions(self.path, std::fs::Permissions::from_mode(0o755)); |
| 10687 | + } |
| 10688 | + } |
| 10689 | + } |
| 10690 | + |
| 10691 | + let context = TestContext::new("3.12"); |
| 10692 | + |
| 10693 | + // Create a parent directory with a `uv.toml` file that sets a non-default resolution. |
| 10694 | + let parent = context.temp_dir.child("parent"); |
| 10695 | + fs_err::create_dir(&parent)?; |
| 10696 | + let config = parent.child("uv.toml"); |
| 10697 | + config.write_str(indoc::indoc! {r#" |
| 10698 | + [pip] |
| 10699 | + resolution = "lowest-direct" |
| 10700 | + index-url = "https://test.pypi.org/simple" |
| 10701 | + "#})?; |
| 10702 | + |
| 10703 | + // Create a child directory to run the command from. |
| 10704 | + let child = parent.child("child"); |
| 10705 | + fs_err::create_dir(&child)?; |
| 10706 | + let requirements_in = child.child("requirements.in"); |
| 10707 | + requirements_in.write_str("anyio>3.0.0")?; |
| 10708 | + |
| 10709 | + // Test 1: Normal permissions - should find and use parent config. |
| 10710 | + let result = context |
| 10711 | + .pip_compile() |
| 10712 | + .arg("--show-settings") |
| 10713 | + .arg("requirements.in") |
| 10714 | + .current_dir(&child) |
| 10715 | + .output()?; |
| 10716 | + assert!(result.status.success()); |
| 10717 | + let stdout = std::str::from_utf8(&result.stdout)?; |
| 10718 | + // Should contain the parent config's resolution. |
| 10719 | + assert!(stdout.contains("resolution: LowestDirect")); |
| 10720 | + assert!(stdout.contains("test.pypi.org")); |
| 10721 | + |
| 10722 | + // Test 2: Permission denied - should gracefully skip parent config. |
| 10723 | + // Try to remove read permissions from the config file itself (more targeted approach) |
| 10724 | + let permission_denied_setup = |
| 10725 | + fs_err::set_permissions(config.path(), std::fs::Permissions::from_mode(0o000)).is_ok(); |
| 10726 | + |
| 10727 | + let _guard = PermissionGuard { |
| 10728 | + path: config.path(), |
| 10729 | + should_restore: permission_denied_setup, |
| 10730 | + }; |
| 10731 | + |
| 10732 | + let result = context |
| 10733 | + .pip_compile() |
| 10734 | + .arg("--show-settings") |
| 10735 | + .arg("requirements.in") |
| 10736 | + .current_dir(&child) |
| 10737 | + .output()?; |
| 10738 | + |
| 10739 | + assert!(result.status.success()); |
| 10740 | + let stdout = std::str::from_utf8(&result.stdout)?; |
| 10741 | + let stderr = std::str::from_utf8(&result.stderr)?; |
| 10742 | + |
| 10743 | + // Should use default resolution (not parent config) if permissions were denied. |
| 10744 | + if permission_denied_setup { |
| 10745 | + assert!(stdout.contains("resolution: Highest")); |
| 10746 | + assert!(!stdout.contains("test.pypi.org")); |
| 10747 | + |
| 10748 | + // Should warn about permission denied (it's an error condition). |
| 10749 | + assert!(stderr.contains("Permission denied while reading workspace configuration")); |
| 10750 | + assert!(stderr.contains("continuing search")); |
| 10751 | + } else { |
| 10752 | + // If we couldn't set permissions, the test behaves like normal case |
| 10753 | + assert!(stdout.contains("resolution: LowestDirect")); |
| 10754 | + assert!(stdout.contains("test.pypi.org")); |
| 10755 | + assert!(!stderr.contains("Permission denied")); |
| 10756 | + } |
| 10757 | + |
| 10758 | + // Test 3: UV_NO_CONFIG - should skip all config discovery silently. |
| 10759 | + let result = context |
| 10760 | + .pip_compile() |
| 10761 | + .arg("--show-settings") |
| 10762 | + .arg("requirements.in") |
| 10763 | + .arg("--no-config") |
| 10764 | + .current_dir(&child) |
| 10765 | + .output()?; |
| 10766 | + |
| 10767 | + assert!(result.status.success()); |
| 10768 | + let stdout = std::str::from_utf8(&result.stdout)?; |
| 10769 | + let stderr = std::str::from_utf8(&result.stderr)?; |
| 10770 | + assert!(stdout.contains("resolution: Highest")); |
| 10771 | + assert!(!stdout.contains("test.pypi.org")); |
| 10772 | + assert!(!stderr.contains("Permission denied")); // UV_NO_CONFIG is silent |
| 10773 | + |
| 10774 | + Ok(()) |
| 10775 | +} |
0 commit comments