diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index ac41dd14d..7fe5f937c 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -110,6 +110,18 @@ fn lock_wheel_registry() -> Result<()> { Resolved 4 packages in [TIME] "###); + // Re-run with `--offline`. We shouldn't need a network connection to validate an + // already-correct lockfile with immutable metadata. + uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Resolved 4 packages in [TIME] + "###); + // Install from the lockfile. uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true @@ -288,6 +300,19 @@ fn lock_sdist_git() -> Result<()> { Resolved 2 packages in [TIME] "###); + // Re-run with `--offline`. We shouldn't need a network connection to validate an + // already-correct lockfile with immutable metadata. + uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + warning: `uv.sources` is experimental and may change without warning + Resolved 2 packages in [TIME] + "###); + // Install from the lockfile. uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true @@ -318,15 +343,15 @@ fn lock_sdist_git() -> Result<()> { )?; uv_snapshot!(context.filters(), context.lock(), @r###" - success: true - exit_code: 0 - ----- stdout ----- + success: true + exit_code: 0 + ----- stdout ----- - ----- stderr ----- - warning: `uv lock` is experimental and may change without warning - warning: `uv.sources` is experimental and may change without warning - Resolved 2 packages in [TIME] - "###); + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + warning: `uv.sources` is experimental and may change without warning + Resolved 2 packages in [TIME] + "###); let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); @@ -376,15 +401,15 @@ fn lock_sdist_git() -> Result<()> { )?; uv_snapshot!(context.filters(), context.lock(), @r###" - success: true - exit_code: 0 - ----- stdout ----- + success: true + exit_code: 0 + ----- stdout ----- - ----- stderr ----- - warning: `uv lock` is experimental and may change without warning - warning: `uv.sources` is experimental and may change without warning - Resolved 2 packages in [TIME] - "###); + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + warning: `uv.sources` is experimental and may change without warning + Resolved 2 packages in [TIME] + "###); let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); @@ -434,15 +459,15 @@ fn lock_sdist_git() -> Result<()> { )?; uv_snapshot!(context.filters(), context.lock(), @r###" - success: true - exit_code: 0 - ----- stdout ----- + success: true + exit_code: 0 + ----- stdout ----- - ----- stderr ----- - warning: `uv lock` is experimental and may change without warning - warning: `uv.sources` is experimental and may change without warning - Resolved 2 packages in [TIME] - "###); + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + warning: `uv.sources` is experimental and may change without warning + Resolved 2 packages in [TIME] + "###); let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); @@ -497,14 +522,14 @@ fn lock_sdist_git_subdirectory() -> Result<()> { )?; uv_snapshot!(context.filters(), context.lock(), @r###" - success: true - exit_code: 0 - ----- stdout ----- + success: true + exit_code: 0 + ----- stdout ----- - ----- stderr ----- - warning: `uv lock` is experimental and may change without warning - Resolved 2 packages in [TIME] - "###); + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Resolved 2 packages in [TIME] + "###); let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); @@ -584,14 +609,14 @@ fn lock_sdist_git_pep508() -> Result<()> { )?; uv_snapshot!(context.filters(), context.lock(), @r###" - success: true - exit_code: 0 - ----- stdout ----- + success: true + exit_code: 0 + ----- stdout ----- - ----- stderr ----- - warning: `uv lock` is experimental and may change without warning - Resolved 2 packages in [TIME] - "###); + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Resolved 2 packages in [TIME] + "###); let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); @@ -649,14 +674,14 @@ fn lock_sdist_git_pep508() -> Result<()> { )?; uv_snapshot!(context.filters(), context.lock(), @r###" - success: true - exit_code: 0 - ----- stdout ----- + success: true + exit_code: 0 + ----- stdout ----- - ----- stderr ----- - warning: `uv lock` is experimental and may change without warning - Resolved 2 packages in [TIME] - "###); + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Resolved 2 packages in [TIME] + "###); let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); @@ -703,14 +728,14 @@ fn lock_sdist_git_pep508() -> Result<()> { )?; uv_snapshot!(context.filters(), context.lock(), @r###" - success: true - exit_code: 0 - ----- stdout ----- + success: true + exit_code: 0 + ----- stdout ----- - ----- stderr ----- - warning: `uv lock` is experimental and may change without warning - Resolved 2 packages in [TIME] - "###); + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Resolved 2 packages in [TIME] + "###); let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); @@ -757,14 +782,14 @@ fn lock_sdist_git_pep508() -> Result<()> { )?; uv_snapshot!(context.filters(), context.lock(), @r###" - success: true - exit_code: 0 - ----- stdout ----- + success: true + exit_code: 0 + ----- stdout ----- - ----- stderr ----- - warning: `uv lock` is experimental and may change without warning - Resolved 2 packages in [TIME] - "###); + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Resolved 2 packages in [TIME] + "###); let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); @@ -917,6 +942,18 @@ fn lock_wheel_url() -> Result<()> { Resolved 4 packages in [TIME] "###); + // Re-run with `--offline`. This should fail: we need network access to resolve mutable metadata. + uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + error: Failed to download: `anyio @ https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl` + Caused by: Network connectivity is disabled, but the requested data wasn't found in the cache for: `https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl` + "###); + // Install from the lockfile. uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true @@ -8460,18 +8497,6 @@ fn lock_remove_member() -> Result<()> { fn lock_add_member() -> Result<()> { let context = TestContext::new("3.12"); - // Create a workspace member. - let leaf = context.temp_dir.child("leaf"); - leaf.child("pyproject.toml").write_str( - r#" - [project] - name = "leaf" - version = "0.1.0" - requires-python = ">=3.12" - dependencies = ["anyio>3"] - "#, - )?; - // Create a workspace, but don't add the member. let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str( @@ -8529,6 +8554,18 @@ fn lock_add_member() -> Result<()> { Resolved 1 package in [TIME] "###); + // Create a workspace member. + let leaf = context.temp_dir.child("leaf"); + leaf.child("pyproject.toml").write_str( + r#" + [project] + name = "leaf" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio>3"] + "#, + )?; + // Add the member to the workspace, but not as a dependency of the root. pyproject_toml.write_str( r#" @@ -8555,6 +8592,21 @@ fn lock_add_member() -> Result<()> { error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`. "###); + // Re-run with `--offline`. This should also fail, during the resolve phase. + uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + × No solution found when resolving dependencies: + ╰─▶ Because anyio was not found in the cache and leaf depends on anyio>3, we can conclude that leaf's requirements are unsatisfiable. + And because your workspace requires leaf, we can conclude that your workspace's requirements are unsatisfiable. + + hint: Packages were unavailable because the network was disabled + "###); + // Re-run without `--locked`. uv_snapshot!(context.filters(), context.lock(), @r###" success: true @@ -8570,6 +8622,17 @@ fn lock_add_member() -> Result<()> { Added sniffio v1.3.1 "###); + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Resolved 5 packages in [TIME] + "###); + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); insta::with_settings!({ @@ -8642,6 +8705,210 @@ fn lock_add_member() -> Result<()> { Ok(()) } +/// Lock a `pyproject.toml`, then add a dependency that's already included in the resolution. +/// In theory, we shouldn't need to re-resolve, but based on our current strategy, we don't accept +/// the existing lockfile. +#[test] +fn lock_redundant_add_member() -> Result<()> { + let context = TestContext::new("3.12"); + + // Lock `anyio`. + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio"] + + [tool.uv.workspace] + members = [] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Resolved 4 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25 00:00:00 UTC" + + [[package]] + name = "anyio" + version = "4.3.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "anyio" }, + ] + + [package.metadata] + requires-dist = [{ name = "anyio" }] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] + "### + ); + }); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Resolved 4 packages in [TIME] + "###); + + // Add a dependency that's already included in the lockfile. + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio", "idna"] + + [tool.uv.workspace] + members = [] + "#, + )?; + + // Re-run with `--locked`. This will fail, though in theory it could succeed, since the current + // _resolution_ satisfies the requirements, even if the inputs are not identical + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Resolved 4 packages in [TIME] + error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`. + "###); + + // Re-run without `--locked`. + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Resolved 4 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25 00:00:00 UTC" + + [[package]] + name = "anyio" + version = "4.3.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "anyio" }, + { name = "idna" }, + ] + + [package.metadata] + requires-dist = [ + { name = "anyio" }, + { name = "idna" }, + ] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] + "### + ); + }); + + Ok(()) +} + /// Lock a `pyproject.toml`, add a new constraint, and ensure that the lockfile is updated on the /// next run. #[test]