diff --git a/crates/uv/tests/workspace.rs b/crates/uv/tests/workspace.rs index cf9e1f622..3a7e91f2d 100644 --- a/crates/uv/tests/workspace.rs +++ b/crates/uv/tests/workspace.rs @@ -1151,3 +1151,234 @@ fn workspace_inherit_sources() -> Result<()> { Ok(()) } + +/// Tests error messages when a workspace member's dependencies cannot be resolved. +#[test] +#[cfg(feature = "pypi")] +fn workspace_unsatisfiable_member_dependencies() -> Result<()> { + let context = TestContext::new("3.12"); + + // Create the workspace root. + let workspace = context.temp_dir.child("workspace"); + workspace.child("pyproject.toml").write_str(indoc! {r#" + [project] + name = "workspace" + version = "0.1.0" + dependencies = [] + requires-python = ">=3.12" + + [tool.uv.workspace] + members = ["packages/*"] + "#})?; + workspace.child("src/__init__.py").touch()?; + + // Create a package that requires a dependency that does not exist. + let leaf = workspace.child("packages").child("leaf"); + leaf.child("pyproject.toml").write_str(indoc! {r#" + [project] + name = "leaf" + version = "0.1.0" + dependencies = ["httpx>9999"] + "#})?; + leaf.child("src/__init__.py").touch()?; + + // Resolving should fail. + uv_snapshot!(context.filters(), context.lock().arg("--preview").current_dir(&workspace), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] + × No solution found when resolving dependencies: + ╰─▶ Because only httpx<=9999 is available and leaf==0.1.0 depends on httpx>9999, we can conclude that leaf==0.1.0 cannot be used. + And because only leaf==0.1.0 is available and you require leaf, we can conclude that the requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Tests error messages when a workspace member's dependencies conflict with +/// another member's. +#[test] +#[cfg(feature = "pypi")] +fn workspace_unsatisfiable_member_dependencies_conflicting() -> Result<()> { + let context = TestContext::new("3.12"); + + // Create the workspace root. + let workspace = context.temp_dir.child("workspace"); + workspace.child("pyproject.toml").write_str(indoc! {r#" + [project] + name = "workspace" + version = "0.1.0" + dependencies = [] + requires-python = ">=3.12" + + [tool.uv.workspace] + members = ["packages/*"] + "#})?; + workspace.child("src/__init__.py").touch()?; + + // Create two workspace members with incompatible pins + let foo = workspace.child("packages").child("foo"); + foo.child("pyproject.toml").write_str(indoc! {r#" + [project] + name = "foo" + version = "0.1.0" + dependencies = ["anyio==4.1.0"] + "#})?; + foo.child("src/__init__.py").touch()?; + let bar = workspace.child("packages").child("bar"); + bar.child("pyproject.toml").write_str(indoc! {r#" + [project] + name = "bar" + version = "0.1.0" + dependencies = ["anyio==4.2.0"] + "#})?; + bar.child("src/__init__.py").touch()?; + + // Resolving should fail. + uv_snapshot!(context.filters(), context.lock().arg("--preview").current_dir(&workspace), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] + × No solution found when resolving dependencies: + ╰─▶ Because only bar==0.1.0 is available and bar==0.1.0 depends on anyio==4.2.0, we can conclude that all versions of bar depend on anyio==4.2.0. + And because foo==0.1.0 depends on anyio==4.1.0 and only foo==0.1.0 is available, we can conclude that all versions of bar and all versions of foo are incompatible. + And because you require bar and foo, we can conclude that the requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Tests error messages when a workspace member's dependencies conflict with +/// two other member's. +#[test] +#[cfg(feature = "pypi")] +fn workspace_unsatisfiable_member_dependencies_conflicting_threeway() -> Result<()> { + let context = TestContext::new("3.12"); + + // Create the workspace root. + let workspace = context.temp_dir.child("workspace"); + workspace.child("pyproject.toml").write_str(indoc! {r#" + [project] + name = "workspace" + version = "0.1.0" + dependencies = [] + requires-python = ">=3.12" + + [tool.uv.workspace] + members = ["packages/*"] + "#})?; + workspace.child("src/__init__.py").touch()?; + + // Create three workspace members with incompatible pins. + let red = workspace.child("packages").child("red"); + red.child("pyproject.toml").write_str(indoc! {r#" + [project] + name = "red" + version = "0.1.0" + dependencies = ["anyio==4.1.0"] + "#})?; + red.child("src/__init__.py").touch()?; + let knot = workspace.child("packages").child("knot"); + knot.child("pyproject.toml").write_str(indoc! {r#" + [project] + name = "knot" + version = "0.1.0" + dependencies = ["anyio==4.2.0"] + "#})?; + knot.child("src/__init__.py").touch()?; + + // We'll raise the first conflict in the resolver, so `bird` shouldn't be + // present in the error even though it also incompatible + let bird = workspace.child("packages").child("bird"); + bird.child("pyproject.toml").write_str(indoc! {r#" + [project] + name = "bird" + version = "0.1.0" + dependencies = ["anyio==4.3.0"] + "#})?; + bird.child("src/__init__.py").touch()?; + + // Resolving should fail. + uv_snapshot!(context.filters(), context.lock().arg("--preview").current_dir(&workspace), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] + × No solution found when resolving dependencies: + ╰─▶ Because only bird==0.1.0 is available and bird==0.1.0 depends on anyio==4.3.0, we can conclude that all versions of bird depend on anyio==4.3.0. + And because knot==0.1.0 depends on anyio==4.2.0 and only knot==0.1.0 is available, we can conclude that all versions of bird and all versions of knot are incompatible. + And because you require bird and knot, we can conclude that the requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Tests error messages when a workspace member's name shadows a dependency of +/// another member. +#[test] +#[cfg(feature = "pypi")] +fn workspace_member_name_shadows_dependencies() -> Result<()> { + let context = TestContext::new("3.12"); + + // Create the workspace root. + let workspace = context.temp_dir.child("workspace"); + workspace.child("pyproject.toml").write_str(indoc! {r#" + [project] + name = "workspace" + version = "0.1.0" + dependencies = [] + requires-python = ">=3.12" + + [tool.uv.workspace] + members = ["packages/*"] + "#})?; + workspace.child("src/__init__.py").touch()?; + + // Create a workspace member that depends on `anyio` + let foo = workspace.child("packages").child("foo"); + foo.child("pyproject.toml").write_str(indoc! {r#" + [project] + name = "foo" + version = "0.1.0" + dependencies = ["anyio==4.1.0"] + "#})?; + foo.child("src/__init__.py").touch()?; + + // Then create an `anyio` workspace member + let anyio = workspace.child("packages").child("anyio"); + anyio.child("pyproject.toml").write_str(indoc! {r#" + [project] + name = "anyio" + version = "0.1.0" + dependencies = [] + "#})?; + anyio.child("src/__init__.py").touch()?; + + // We should fail + // TODO(zanieb): This error message is bad? + uv_snapshot!(context.filters(), context.lock().arg("--preview").current_dir(&workspace), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] + error: Failed to download and build: `foo @ file://[TEMP_DIR]/workspace/packages/foo` + Caused by: Failed to parse entry for: `anyio` + Caused by: Package is not included as workspace package in `tool.uv.workspace` + "### + ); + + Ok(()) +}