From a502d28aa43890cdc24e015c74475e04bbeb1149 Mon Sep 17 00:00:00 2001 From: Jonathan Yoder Date: Thu, 24 Sep 2026 10:51:05 -0400 Subject: [PATCH 1/2] test(resolver): vendor packse scenarios and cross-check against a SAT oracle Vendors astral-sh/packse's 147 non-example scenario fixtures under resolver/testdata/packse (pinned commit and refresh steps in its README), loads them with BurntSushi/toml, and runs the non-universal ones against resolver.Resolve. A second, independent CNF encoding built from the same scenario data and solved with crillab/gophersat cross-checks the resolver's own answer in both directions. 41 universal scenarios are out of scope (RFD 0001 defers universal resolution). 4 use no_build/no_binary, which resolver.Options has no knob for. 8 run and are asserted to still disagree with packse's expected result, each with a one-line reason (PEP 592's yanked exact-pin exception, an extras edge case, a pre-release fallback pip/uv apply that candidate.PrereleaseSet does not, and Requires-Python upper bounds, which uv ignores and go-pyresolver does not). The remaining 94 pass exactly. Refs rstudio/package-manager#18658 Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/ci.yml | 22 + NOTICE | 14 + go.mod | 2 + go.sum | 4 + resolver/packse_scenario_test.go | 369 ++++++++++++++ resolver/packse_test.go | 261 ++++++++++ resolver/satoracle_test.go | 466 ++++++++++++++++++ resolver/testdata/packse/LICENSE-APACHE | 201 ++++++++ resolver/testdata/packse/LICENSE-MIT | 21 + resolver/testdata/packse/README.md | 41 ++ .../backtrack-to-missing-package.toml | 23 + .../backtrack-with-missing-package.toml | 27 + .../wrong-backtracking-basic.toml | 38 ++ .../wrong-backtracking-indirect.toml | 45 ++ ...requires-exact-version-does-not-exist.toml | 10 + ...quires-greater-version-does-not-exist.toml | 12 + .../requires-less-version-does-not-exist.toml | 14 + .../requires-package-does-not-exist.toml | 10 + ...itive-requires-package-does-not-exist.toml | 11 + ...ntiguous-range-of-compatible-versions.toml | 42 ++ ...excludes-range-of-compatible-versions.toml | 39 ++ .../excluded-only-compatible-version.toml | 24 + .../excluded/excluded-only-version.toml | 11 + .../packse/extras/all-extras-required.toml | 22 + .../extra-does-not-exist-backtrack.toml | 21 + ...incompatible-with-extra-not-requested.toml | 21 + .../extras/extra-incompatible-with-extra.toml | 17 + .../extras/extra-incompatible-with-root.toml | 16 + .../packse/extras/extra-required.toml | 17 + .../testdata/packse/extras/missing-extra.toml | 14 + .../extras/multiple-extras-required.toml | 21 + ...flicting-non-overlapping-dependencies.toml | 24 + ...non-conflicting-repeated-dependencies.toml | 26 + resolver/testdata/packse/fork/basic.toml | 17 + .../packse/fork/conflict-in-fork.toml | 26 + .../packse/fork/conflict-unsatisfiable.toml | 23 + .../fork/filter-sibling-dependencies.toml | 39 ++ .../testdata/packse/fork/fork-upgrade.toml | 28 ++ .../packse/fork/incomplete-markers.toml | 29 ++ .../testdata/packse/fork/marker-accrue.toml | 28 ++ .../testdata/packse/fork/marker-disjoint.toml | 26 + .../fork/marker-inherit-combined-allowed.toml | 31 ++ .../marker-inherit-combined-disallowed.toml | 32 ++ .../packse/fork/marker-inherit-combined.toml | 33 ++ .../packse/fork/marker-inherit-isolated.toml | 25 + .../fork/marker-inherit-transitive.toml | 30 ++ .../testdata/packse/fork/marker-inherit.toml | 28 ++ .../packse/fork/marker-limited-inherit.toml | 34 ++ .../packse/fork/marker-selection.toml | 28 ++ .../testdata/packse/fork/marker-track.toml | 30 ++ .../fork/non-fork-marker-transitive.toml | 24 + .../fork/non-local-fork-marker-direct.toml | 28 ++ .../non-local-fork-marker-transitive.toml | 31 ++ .../fork/overlapping-markers-basic.toml | 48 ++ ...references-dependent-forking-bistable.toml | 77 +++ ...erences-dependent-forking-conflicting.toml | 79 +++ ...eferences-dependent-forking-tristable.toml | 86 ++++ .../fork/preferences-dependent-forking.toml | 76 +++ .../fork/remaining-universe-partitioning.toml | 47 ++ .../fork/requires-python-full-prerelease.toml | 23 + .../packse/fork/requires-python-full.toml | 23 + .../fork/requires-python-patch-overlap.toml | 28 ++ .../testdata/packse/fork/requires-python.toml | 20 + .../direct-incompatible-versions.toml | 12 + .../transitive-incompatible-versions.toml | 11 + ...sitive-incompatible-with-root-version.toml | 15 + ...ansitive-incompatible-with-transitive.toml | 18 + .../local/local-greater-than-or-equal.toml | 16 + .../packse/local/local-greater-than.toml | 10 + .../local/local-less-than-or-equal.toml | 16 + .../packse/local/local-less-than.toml | 10 + .../packse/local/local-not-latest.toml | 22 + .../local/local-not-used-with-sdist.toml | 18 + .../testdata/packse/local/local-simple.toml | 16 + .../local/local-transitive-backtrack.toml | 23 + .../local/local-transitive-conflicting.toml | 15 + .../local/local-transitive-confounding.toml | 24 + ...ocal-transitive-greater-than-or-equal.toml | 20 + .../local/local-transitive-greater-than.toml | 15 + .../local-transitive-less-than-or-equal.toml | 20 + .../local/local-transitive-less-than.toml | 15 + .../packse/local/local-transitive.toml | 20 + .../local/local-used-without-sdist.toml | 18 + .../packse/post/post-equal-available.toml | 16 + .../packse/post/post-equal-not-available.toml | 12 + .../post/post-greater-than-or-equal-post.toml | 16 + .../post/post-greater-than-or-equal.toml | 14 + .../post-greater-than-post-not-available.toml | 14 + .../packse/post/post-greater-than-post.toml | 16 + .../packse/post/post-greater-than.toml | 10 + .../packse/post/post-less-than-or-equal.toml | 10 + .../testdata/packse/post/post-less-than.toml | 10 + .../post/post-local-greater-than-post.toml | 12 + .../packse/post/post-local-greater-than.toml | 12 + .../testdata/packse/post/post-simple.toml | 10 + .../package-multiple-prereleases-kinds.toml | 18 + .../package-multiple-prereleases-numbers.toml | 18 + .../package-only-prereleases-boundary.toml | 18 + .../package-only-prereleases-in-range.toml | 13 + .../prereleases/package-only-prereleases.toml | 14 + ...-prerelease-specified-mixed-available.toml | 20 + ...elease-specified-only-final-available.toml | 18 + ...e-specified-only-prerelease-available.toml | 18 + .../package-prereleases-boundary.toml | 21 + .../package-prereleases-global-boundary.toml | 21 + ...ackage-prereleases-specifier-boundary.toml | 24 + ...ly-prereleases-in-range-global-opt-in.toml | 18 + ...ires-package-prerelease-and-final-any.toml | 16 + ...kage-only-prereleases-in-range-opt-in.toml | 20 + ...ive-package-only-prereleases-in-range.toml | 16 + .../transitive-package-only-prereleases.toml | 18 + ...stable-dependency-many-versions-holes.toml | 53 ++ ...e-and-stable-dependency-many-versions.toml | 53 ++ ...erelease-and-stable-dependency-opt-in.toml | 24 + ...tive-prerelease-and-stable-dependency.toml | 19 + ...mpatible-python-incompatible-override.toml | 17 + ...mpatible-override-available-no-wheels.toml | 23 + ...patible-override-no-compatible-wheels.toml | 19 + ...ython-compatible-override-other-wheel.toml | 22 + ...atible-override-unavailable-no-wheels.toml | 19 + ...compatible-python-compatible-override.toml | 20 + ...python-greater-than-current-backtrack.toml | 26 + .../python-greater-than-current-excluded.toml | 23 + .../python-greater-than-current-many.toml | 47 ++ .../python-greater-than-current-patch.toml | 14 + .../python-greater-than-current.toml | 14 + .../python-less-than-current.toml | 15 + .../python-patch-override-no-patch.toml | 18 + ...ython-patch-override-patch-compatible.toml | 20 + .../python-version-does-not-exist.toml | 11 + .../requires-python-wheels.toml | 19 + .../tag_and_markers/unreachable-package.toml | 16 + .../tag_and_markers/unreachable-wheels.toml | 39 ++ .../virtual-package-extra-priorities.toml | 26 + .../virtual-package-marker-priorities.toml | 23 + .../testdata/packse/wheels/no-binary.toml | 14 + resolver/testdata/packse/wheels/no-build.toml | 14 + .../no-sdist-no-wheels-with-matching-abi.toml | 15 + ...dist-no-wheels-with-matching-platform.toml | 15 + ...-sdist-no-wheels-with-matching-python.toml | 15 + .../packse/wheels/no-wheels-no-build.toml | 14 + .../no-wheels-with-matching-platform.toml | 11 + .../testdata/packse/wheels/no-wheels.toml | 11 + .../packse/wheels/only-wheels-no-binary.toml | 14 + .../testdata/packse/wheels/only-wheels.toml | 11 + .../packse/wheels/requires-python-subset.toml | 16 + .../packse/wheels/specific-architecture.toml | 54 ++ .../wheels/specific-tag-and-default.toml | 14 + .../yanked/package-only-yanked-in-range.toml | 14 + .../packse/yanked/package-only-yanked.toml | 12 + ...kage-yanked-specified-mixed-available.toml | 22 + ...uires-package-yanked-and-unyanked-any.toml | 17 + ...e-package-only-yanked-in-range-opt-in.toml | 21 + ...ansitive-package-only-yanked-in-range.toml | 17 + .../transitive-package-only-yanked.toml | 15 + ...yanked-and-unyanked-dependency-opt-in.toml | 25 + ...sitive-yanked-and-unyanked-dependency.toml | 20 + 157 files changed, 4721 insertions(+) create mode 100644 resolver/packse_scenario_test.go create mode 100644 resolver/packse_test.go create mode 100644 resolver/satoracle_test.go create mode 100644 resolver/testdata/packse/LICENSE-APACHE create mode 100644 resolver/testdata/packse/LICENSE-MIT create mode 100644 resolver/testdata/packse/README.md create mode 100644 resolver/testdata/packse/backtracking/backtrack-to-missing-package.toml create mode 100644 resolver/testdata/packse/backtracking/backtrack-with-missing-package.toml create mode 100644 resolver/testdata/packse/backtracking/wrong-backtracking-basic.toml create mode 100644 resolver/testdata/packse/backtracking/wrong-backtracking-indirect.toml create mode 100644 resolver/testdata/packse/does_not_exist/requires-exact-version-does-not-exist.toml create mode 100644 resolver/testdata/packse/does_not_exist/requires-greater-version-does-not-exist.toml create mode 100644 resolver/testdata/packse/does_not_exist/requires-less-version-does-not-exist.toml create mode 100644 resolver/testdata/packse/does_not_exist/requires-package-does-not-exist.toml create mode 100644 resolver/testdata/packse/does_not_exist/transitive-requires-package-does-not-exist.toml create mode 100644 resolver/testdata/packse/excluded/dependency-excludes-non-contiguous-range-of-compatible-versions.toml create mode 100644 resolver/testdata/packse/excluded/dependency-excludes-range-of-compatible-versions.toml create mode 100644 resolver/testdata/packse/excluded/excluded-only-compatible-version.toml create mode 100644 resolver/testdata/packse/excluded/excluded-only-version.toml create mode 100644 resolver/testdata/packse/extras/all-extras-required.toml create mode 100644 resolver/testdata/packse/extras/extra-does-not-exist-backtrack.toml create mode 100644 resolver/testdata/packse/extras/extra-incompatible-with-extra-not-requested.toml create mode 100644 resolver/testdata/packse/extras/extra-incompatible-with-extra.toml create mode 100644 resolver/testdata/packse/extras/extra-incompatible-with-root.toml create mode 100644 resolver/testdata/packse/extras/extra-required.toml create mode 100644 resolver/testdata/packse/extras/missing-extra.toml create mode 100644 resolver/testdata/packse/extras/multiple-extras-required.toml create mode 100644 resolver/testdata/packse/fork/allows-non-conflicting-non-overlapping-dependencies.toml create mode 100644 resolver/testdata/packse/fork/allows-non-conflicting-repeated-dependencies.toml create mode 100644 resolver/testdata/packse/fork/basic.toml create mode 100644 resolver/testdata/packse/fork/conflict-in-fork.toml create mode 100644 resolver/testdata/packse/fork/conflict-unsatisfiable.toml create mode 100644 resolver/testdata/packse/fork/filter-sibling-dependencies.toml create mode 100644 resolver/testdata/packse/fork/fork-upgrade.toml create mode 100644 resolver/testdata/packse/fork/incomplete-markers.toml create mode 100644 resolver/testdata/packse/fork/marker-accrue.toml create mode 100644 resolver/testdata/packse/fork/marker-disjoint.toml create mode 100644 resolver/testdata/packse/fork/marker-inherit-combined-allowed.toml create mode 100644 resolver/testdata/packse/fork/marker-inherit-combined-disallowed.toml create mode 100644 resolver/testdata/packse/fork/marker-inherit-combined.toml create mode 100644 resolver/testdata/packse/fork/marker-inherit-isolated.toml create mode 100644 resolver/testdata/packse/fork/marker-inherit-transitive.toml create mode 100644 resolver/testdata/packse/fork/marker-inherit.toml create mode 100644 resolver/testdata/packse/fork/marker-limited-inherit.toml create mode 100644 resolver/testdata/packse/fork/marker-selection.toml create mode 100644 resolver/testdata/packse/fork/marker-track.toml create mode 100644 resolver/testdata/packse/fork/non-fork-marker-transitive.toml create mode 100644 resolver/testdata/packse/fork/non-local-fork-marker-direct.toml create mode 100644 resolver/testdata/packse/fork/non-local-fork-marker-transitive.toml create mode 100644 resolver/testdata/packse/fork/overlapping-markers-basic.toml create mode 100644 resolver/testdata/packse/fork/preferences-dependent-forking-bistable.toml create mode 100644 resolver/testdata/packse/fork/preferences-dependent-forking-conflicting.toml create mode 100644 resolver/testdata/packse/fork/preferences-dependent-forking-tristable.toml create mode 100644 resolver/testdata/packse/fork/preferences-dependent-forking.toml create mode 100644 resolver/testdata/packse/fork/remaining-universe-partitioning.toml create mode 100644 resolver/testdata/packse/fork/requires-python-full-prerelease.toml create mode 100644 resolver/testdata/packse/fork/requires-python-full.toml create mode 100644 resolver/testdata/packse/fork/requires-python-patch-overlap.toml create mode 100644 resolver/testdata/packse/fork/requires-python.toml create mode 100644 resolver/testdata/packse/incompatible_versions/direct-incompatible-versions.toml create mode 100644 resolver/testdata/packse/incompatible_versions/transitive-incompatible-versions.toml create mode 100644 resolver/testdata/packse/incompatible_versions/transitive-incompatible-with-root-version.toml create mode 100644 resolver/testdata/packse/incompatible_versions/transitive-incompatible-with-transitive.toml create mode 100644 resolver/testdata/packse/local/local-greater-than-or-equal.toml create mode 100644 resolver/testdata/packse/local/local-greater-than.toml create mode 100644 resolver/testdata/packse/local/local-less-than-or-equal.toml create mode 100644 resolver/testdata/packse/local/local-less-than.toml create mode 100644 resolver/testdata/packse/local/local-not-latest.toml create mode 100644 resolver/testdata/packse/local/local-not-used-with-sdist.toml create mode 100644 resolver/testdata/packse/local/local-simple.toml create mode 100644 resolver/testdata/packse/local/local-transitive-backtrack.toml create mode 100644 resolver/testdata/packse/local/local-transitive-conflicting.toml create mode 100644 resolver/testdata/packse/local/local-transitive-confounding.toml create mode 100644 resolver/testdata/packse/local/local-transitive-greater-than-or-equal.toml create mode 100644 resolver/testdata/packse/local/local-transitive-greater-than.toml create mode 100644 resolver/testdata/packse/local/local-transitive-less-than-or-equal.toml create mode 100644 resolver/testdata/packse/local/local-transitive-less-than.toml create mode 100644 resolver/testdata/packse/local/local-transitive.toml create mode 100644 resolver/testdata/packse/local/local-used-without-sdist.toml create mode 100644 resolver/testdata/packse/post/post-equal-available.toml create mode 100644 resolver/testdata/packse/post/post-equal-not-available.toml create mode 100644 resolver/testdata/packse/post/post-greater-than-or-equal-post.toml create mode 100644 resolver/testdata/packse/post/post-greater-than-or-equal.toml create mode 100644 resolver/testdata/packse/post/post-greater-than-post-not-available.toml create mode 100644 resolver/testdata/packse/post/post-greater-than-post.toml create mode 100644 resolver/testdata/packse/post/post-greater-than.toml create mode 100644 resolver/testdata/packse/post/post-less-than-or-equal.toml create mode 100644 resolver/testdata/packse/post/post-less-than.toml create mode 100644 resolver/testdata/packse/post/post-local-greater-than-post.toml create mode 100644 resolver/testdata/packse/post/post-local-greater-than.toml create mode 100644 resolver/testdata/packse/post/post-simple.toml create mode 100644 resolver/testdata/packse/prereleases/package-multiple-prereleases-kinds.toml create mode 100644 resolver/testdata/packse/prereleases/package-multiple-prereleases-numbers.toml create mode 100644 resolver/testdata/packse/prereleases/package-only-prereleases-boundary.toml create mode 100644 resolver/testdata/packse/prereleases/package-only-prereleases-in-range.toml create mode 100644 resolver/testdata/packse/prereleases/package-only-prereleases.toml create mode 100644 resolver/testdata/packse/prereleases/package-prerelease-specified-mixed-available.toml create mode 100644 resolver/testdata/packse/prereleases/package-prerelease-specified-only-final-available.toml create mode 100644 resolver/testdata/packse/prereleases/package-prerelease-specified-only-prerelease-available.toml create mode 100644 resolver/testdata/packse/prereleases/package-prereleases-boundary.toml create mode 100644 resolver/testdata/packse/prereleases/package-prereleases-global-boundary.toml create mode 100644 resolver/testdata/packse/prereleases/package-prereleases-specifier-boundary.toml create mode 100644 resolver/testdata/packse/prereleases/requires-package-only-prereleases-in-range-global-opt-in.toml create mode 100644 resolver/testdata/packse/prereleases/requires-package-prerelease-and-final-any.toml create mode 100644 resolver/testdata/packse/prereleases/transitive-package-only-prereleases-in-range-opt-in.toml create mode 100644 resolver/testdata/packse/prereleases/transitive-package-only-prereleases-in-range.toml create mode 100644 resolver/testdata/packse/prereleases/transitive-package-only-prereleases.toml create mode 100644 resolver/testdata/packse/prereleases/transitive-prerelease-and-stable-dependency-many-versions-holes.toml create mode 100644 resolver/testdata/packse/prereleases/transitive-prerelease-and-stable-dependency-many-versions.toml create mode 100644 resolver/testdata/packse/prereleases/transitive-prerelease-and-stable-dependency-opt-in.toml create mode 100644 resolver/testdata/packse/prereleases/transitive-prerelease-and-stable-dependency.toml create mode 100644 resolver/testdata/packse/requires_python/compatible-python-incompatible-override.toml create mode 100644 resolver/testdata/packse/requires_python/incompatible-python-compatible-override-available-no-wheels.toml create mode 100644 resolver/testdata/packse/requires_python/incompatible-python-compatible-override-no-compatible-wheels.toml create mode 100644 resolver/testdata/packse/requires_python/incompatible-python-compatible-override-other-wheel.toml create mode 100644 resolver/testdata/packse/requires_python/incompatible-python-compatible-override-unavailable-no-wheels.toml create mode 100644 resolver/testdata/packse/requires_python/incompatible-python-compatible-override.toml create mode 100644 resolver/testdata/packse/requires_python/python-greater-than-current-backtrack.toml create mode 100644 resolver/testdata/packse/requires_python/python-greater-than-current-excluded.toml create mode 100644 resolver/testdata/packse/requires_python/python-greater-than-current-many.toml create mode 100644 resolver/testdata/packse/requires_python/python-greater-than-current-patch.toml create mode 100644 resolver/testdata/packse/requires_python/python-greater-than-current.toml create mode 100644 resolver/testdata/packse/requires_python/python-less-than-current.toml create mode 100644 resolver/testdata/packse/requires_python/python-patch-override-no-patch.toml create mode 100644 resolver/testdata/packse/requires_python/python-patch-override-patch-compatible.toml create mode 100644 resolver/testdata/packse/requires_python/python-version-does-not-exist.toml create mode 100644 resolver/testdata/packse/tag_and_markers/requires-python-wheels.toml create mode 100644 resolver/testdata/packse/tag_and_markers/unreachable-package.toml create mode 100644 resolver/testdata/packse/tag_and_markers/unreachable-wheels.toml create mode 100644 resolver/testdata/packse/tag_and_markers/virtual-package-extra-priorities.toml create mode 100644 resolver/testdata/packse/tag_and_markers/virtual-package-marker-priorities.toml create mode 100644 resolver/testdata/packse/wheels/no-binary.toml create mode 100644 resolver/testdata/packse/wheels/no-build.toml create mode 100644 resolver/testdata/packse/wheels/no-sdist-no-wheels-with-matching-abi.toml create mode 100644 resolver/testdata/packse/wheels/no-sdist-no-wheels-with-matching-platform.toml create mode 100644 resolver/testdata/packse/wheels/no-sdist-no-wheels-with-matching-python.toml create mode 100644 resolver/testdata/packse/wheels/no-wheels-no-build.toml create mode 100644 resolver/testdata/packse/wheels/no-wheels-with-matching-platform.toml create mode 100644 resolver/testdata/packse/wheels/no-wheels.toml create mode 100644 resolver/testdata/packse/wheels/only-wheels-no-binary.toml create mode 100644 resolver/testdata/packse/wheels/only-wheels.toml create mode 100644 resolver/testdata/packse/wheels/requires-python-subset.toml create mode 100644 resolver/testdata/packse/wheels/specific-architecture.toml create mode 100644 resolver/testdata/packse/wheels/specific-tag-and-default.toml create mode 100644 resolver/testdata/packse/yanked/package-only-yanked-in-range.toml create mode 100644 resolver/testdata/packse/yanked/package-only-yanked.toml create mode 100644 resolver/testdata/packse/yanked/package-yanked-specified-mixed-available.toml create mode 100644 resolver/testdata/packse/yanked/requires-package-yanked-and-unyanked-any.toml create mode 100644 resolver/testdata/packse/yanked/transitive-package-only-yanked-in-range-opt-in.toml create mode 100644 resolver/testdata/packse/yanked/transitive-package-only-yanked-in-range.toml create mode 100644 resolver/testdata/packse/yanked/transitive-package-only-yanked.toml create mode 100644 resolver/testdata/packse/yanked/transitive-yanked-and-unyanked-dependency-opt-in.toml create mode 100644 resolver/testdata/packse/yanked/transitive-yanked-and-unyanked-dependency.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ec81d5..9b2a697 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,6 +99,28 @@ jobs: echo "::error::The equivalence check did not report a fast-mode comparison, so it did not run." exit 1 fi + # packse scenarios cross-checked against a SAT oracle, run the same way and + # for the same reason as the two steps above: a mistyped -run exits 0 while + # asserting nothing, and a silent SKIP would hide a scenario going untested + # instead of failing loudly. See resolver/testdata/packse/README.md. + - name: packse and SAT oracle (must not skip) + shell: bash + run: | + set -euo pipefail + go test ./resolver/ -run 'TestPackse|TestSATOracle' -v -count=1 -timeout 20m \ + | tee packse.log + if grep -q -e '--- SKIP' packse.log; then + echo "::error::A packse or SAT oracle test skipped. These must run on every PR." + exit 1 + fi + if ! grep -qE 'packse: [0-9]+ scenarios = ' packse.log; then + echo "::error::TestPackse did not print its summary line, so it did not run." + exit 1 + fi + if ! grep -qE 'SAT oracle: cross-checked [0-9]+ scenarios' packse.log; then + echo "::error::TestSATOracle did not print its summary line, so it did not run." + exit 1 + fi - name: Format check run: | unformatted=$(gofmt -l .) diff --git a/NOTICE b/NOTICE index d1c44a9..4766c02 100644 --- a/NOTICE +++ b/NOTICE @@ -48,3 +48,17 @@ github.com/posit-dev/go-python-packaging, used for PEP 440 versions and PEP 508 requirements, is Copyright (c) Posit Software, PBC, licensed under the Apache License, Version 2.0, or the MIT license. See that project's own NOTICE for the upstreams it in turn credits. + +resolver/testdata/packse contains scenario files copied from astral-sh/packse +(https://github.com/astral-sh/packse), Copyright (c) 2023 Astral Software Inc., +dual-licensed under the Apache License, Version 2.0, or the MIT license. See +resolver/testdata/packse/README.md for the pinned commit and how they were +copied. + +github.com/crillab/gophersat, used as a SAT-solver cross-check in the resolver +package's tests, is Copyright (c) 2017 Centre de Recherche en Informatique de +Lens and contributors, licensed under the MIT license. Test-only dependency. + +github.com/BurntSushi/toml, used to parse the packse scenario fixtures in the +resolver package's tests, is licensed under the MIT license. Test-only +dependency. diff --git a/go.mod b/go.mod index 836f658..2d89ae2 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/posit-dev/go-pyresolver go 1.25.0 require ( + github.com/BurntSushi/toml v1.6.0 + github.com/crillab/gophersat v1.4.0 github.com/klauspost/compress v1.19.2 github.com/posit-dev/go-pubgrub v0.2.1 github.com/posit-dev/go-python-packaging v0.7.0 diff --git a/go.sum b/go.sum index 0a7f8d9..b96b8a5 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/crillab/gophersat v1.4.0 h1:irf9ajKmNnEURjgPU4oz+ouqIXXLQ59ZNd3NC+hULMc= +github.com/crillab/gophersat v1.4.0/go.mod h1:gDzeMEBrqJR20IL9JW25tFHNGLU5+GDeJzr0zpi3mxs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8= diff --git a/resolver/packse_scenario_test.go b/resolver/packse_scenario_test.go new file mode 100644 index 0000000..f355404 --- /dev/null +++ b/resolver/packse_scenario_test.go @@ -0,0 +1,369 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT + +package resolver_test + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "testing" + + "github.com/BurntSushi/toml" + "github.com/posit-dev/go-pyresolver/index" + "github.com/posit-dev/go-pyresolver/provider" + "github.com/posit-dev/go-python-packaging/marker" + "github.com/posit-dev/go-python-packaging/tags" +) + +// packseTestdataDir holds the vendored scenario TOML files. See +// testdata/packse/README.md for the pinned upstream commit and refresh steps. +const packseTestdataDir = "testdata/packse" + +// packseDefaultRequiresPython is packse's own default for both the root +// package and every version that omits requires_python. See +// astral-sh/packse's scenario.py: `requires_python: str | None = ">=3.12"`. +const packseDefaultRequiresPython = ">=3.12" + +// packseDefaultPython is packse's default active interpreter when a scenario +// has no [environment] section at all. +const packseDefaultPython = "3.12" + +// tomlScenario mirrors astral-sh/packse's scenario.py Scenario struct field +// for field. It is decoded with toml.DecodeFile and MetaData.Undecoded() is +// checked, matching packse's own forbid_unknown_fields=True: a re-pull that +// adds a field must fail the loader rather than silently drop it. +type tomlScenario struct { + Name string `toml:"name"` + Description string `toml:"description"` + Packages map[string]tomlPackage `toml:"packages"` + Root tomlRoot `toml:"root"` + Expected tomlExpected `toml:"expected"` + Environment tomlEnvironment `toml:"environment"` + ResolverOptions tomlResolverOptions `toml:"resolver_options"` + Template string `toml:"template"` +} + +type tomlPackage struct { + Versions map[string]tomlVersion `toml:"versions"` +} + +type tomlVersion struct { + RequiresPython *string `toml:"requires_python"` + Requires []string `toml:"requires"` + Extras map[string][]string `toml:"extras"` + Sdist *bool `toml:"sdist"` + Wheel *bool `toml:"wheel"` + Yanked bool `toml:"yanked"` + WheelTags []string `toml:"wheel_tags"` + Description string `toml:"description"` +} + +type tomlRoot struct { + RequiresPython *string `toml:"requires_python"` + Requires []string `toml:"requires"` +} + +type tomlExpected struct { + Satisfiable bool `toml:"satisfiable"` + Packages map[string]string `toml:"packages"` + Explanation *string `toml:"explanation"` +} + +type tomlEnvironment struct { + Python string `toml:"python"` + AdditionalPython []string `toml:"additional_python"` +} + +type tomlResolverOptions struct { + Python *string `toml:"python"` + Prereleases bool `toml:"prereleases"` + NoBuild []string `toml:"no_build"` + NoBinary []string `toml:"no_binary"` + Universal bool `toml:"universal"` + PythonPlatform *string `toml:"python_platform"` + RequiredEnvironments []string `toml:"required_environments"` +} + +// packseScenario is one loaded scenario, named by its path relative to +// testdata/packse (e.g. "requires_python/python-less-than-current"), which is +// what the classification lists (outOfScope, knownFail, unsupportedOption) key +// on. +type packseScenario struct { + relName string // category/scenario-name, no extension + path string + scenario tomlScenario +} + +// loadPackseScenarios walks testdata/packse and decodes every scenario TOML +// file, failing the test if any file carries a key this loader does not know +// about -- a silent drop on re-pull is exactly the failure mode packse's own +// forbid_unknown_fields guards against, and the loader must match it. +func loadPackseScenarios(t *testing.T) []packseScenario { + t.Helper() + + var out []packseScenario + err := filepath.WalkDir(packseTestdataDir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() || filepath.Ext(path) != ".toml" { + return nil + } + + var s tomlScenario + md, decErr := toml.DecodeFile(path, &s) + if decErr != nil { + t.Fatalf("decode %s: %v", path, decErr) + } + if undecoded := md.Undecoded(); len(undecoded) > 0 { + keys := make([]string, len(undecoded)) + for i, k := range undecoded { + keys[i] = k.String() + } + t.Fatalf("%s: undecoded keys %v -- packse's schema grew a field this loader "+ + "does not know about; add it rather than silently dropping it", path, keys) + } + + rel, relErr := filepath.Rel(packseTestdataDir, path) + if relErr != nil { + return relErr + } + rel = strings.TrimSuffix(rel, ".toml") + out = append(out, packseScenario{relName: filepath.ToSlash(rel), path: path, scenario: s}) + return nil + }) + if err != nil { + t.Fatalf("walk %s: %v", packseTestdataDir, err) + } + + sort.Slice(out, func(i, j int) bool { return out[i].relName < out[j].relName }) + return out +} + +// pythonSpec is the single Python version this scenario resolves for, and the +// tags.Target derived from it. +// +// ⚠️ resolver.Options carries exactly ONE python version, used both to filter +// Requires-Python and to evaluate markers (Resolve.validate enforces they +// agree). packse/uv can express a resolver_options.python DIFFERENT from +// environment.python -- a "resolve for 3.11 while running 3.9" override. This +// harness cannot represent that split, so it picks resolver_options.python +// when set, else environment.python, matching the brief. A scenario where +// that choice changes the answer packse expects is a knownFail candidate, not +// a bug in the loader. +type pythonSpec struct { + full string // e.g. "3.9.0" + major int + minor int +} + +// scenarioPython derives the pythonSpec for s, per the rule above. +func scenarioPython(s tomlScenario) (pythonSpec, error) { + raw := packseDefaultPython + if s.Environment.Python != "" { + raw = s.Environment.Python + } + if s.ResolverOptions.Python != nil && *s.ResolverOptions.Python != "" { + raw = *s.ResolverOptions.Python + } + + full := raw + parts := strings.Split(raw, ".") + if len(parts) == 2 { + // "3.9" -> "3.9.0": packse's own fixtures document this padding + // (requires_python/python-patch-override-no-patch.toml's explanation: + // "the minimum compatible Python requirement is treated as 3.9.0"). + full = raw + ".0" + } + if len(parts) < 2 { + return pythonSpec{}, fmt.Errorf("python version %q has no minor component", raw) + } + major, err := strconv.Atoi(parts[0]) + if err != nil { + return pythonSpec{}, fmt.Errorf("python version %q: bad major: %w", raw, err) + } + minor, err := strconv.Atoi(parts[1]) + if err != nil { + return pythonSpec{}, fmt.Errorf("python version %q: bad minor: %w", raw, err) + } + return pythonSpec{full: full, major: major, minor: minor}, nil +} + +// packsePlatform maps a resolver_options.python_platform string to a +// tags.Target's OS/Arch. Only the one value the vendored corpus actually uses +// is recognized (measured: all three non-universal python_platform scenarios +// say "x86_64-manylinux2014", which is also this harness's default target); +// anything else is unsupportedOption rather than guessed at. +func packsePlatform(raw string) (os, arch string, ok bool) { + if raw == "x86_64-manylinux2014" { + return "linux", "x86_64", true + } + return "", "", false +} + +// buildTarget builds the tags.Target this scenario resolves for. +func buildTarget(py pythonSpec, platform *string) (tags.Target, error) { + target := tags.Target{ + Implementation: "cp", PyMajor: py.major, PyMinor: py.minor, + OS: "linux", Arch: "x86_64", Libc: "glibc", LibcMajor: 2, LibcMinor: 28, + } + if platform != nil { + os, arch, ok := packsePlatform(*platform) + if !ok { + return tags.Target{}, fmt.Errorf("unsupported python_platform %q", *platform) + } + target.OS, target.Arch = os, arch + } + return target, nil +} + +// buildEnvironment builds the marker.Environment for py, matching testEnv's +// own construction through EnvironmentFromTarget rather than a struct literal +// (see resolver.Options.Environment's doc comment for why a literal is +// wrong). +func buildEnvironment(t tags.Target, py pythonSpec) (marker.Environment, error) { + return marker.EnvironmentFromTarget(t, marker.InterpreterIdentity{ + ImplementationName: "cpython", + PlatformPythonImplementation: "CPython", + PythonFullVersion: py.full, + ImplementationVersion: py.full, + }) +} + +// distFacts is what the harness (and the SAT oracle) derive about one +// version's publications from packse's wheel/sdist/wheel_tags fields. +type distFacts struct { + wheelTags []string + hasSdist bool +} + +func versionDistFacts(v tomlVersion) distFacts { + wheel := true + if v.Wheel != nil { + wheel = *v.Wheel + } + sdist := true + if v.Sdist != nil { + sdist = *v.Sdist + } + + tags := v.WheelTags + if wheel && len(tags) == 0 { + // packse's build.py always produces a "py3-none-any" wheel when + // wheel_tags is empty and wheel=true (its default template package). + tags = []string{"py3-none-any"} + } + if !wheel { + tags = nil + } + return distFacts{wheelTags: tags, hasSdist: sdist} +} + +// requiresPythonOf returns v's Requires-Python, or packse's default. +func requiresPythonOf(v tomlVersion) string { + if v.RequiresPython != nil { + return *v.RequiresPython + } + return packseDefaultRequiresPython +} + +// extraRequirement rewrites req to be conditional on extra being requested, +// combining with any marker req already carries. packse's schema stores extra +// requirements separately from a version's own "requires" (unlike a real +// published package, which folds both into one Requires-Dist list with +// "; extra == ..." markers) -- this is what puts them back together for +// go-pyresolver, which only understands the folded form. +func extraRequirement(req, extra string) string { + return fmt.Sprintf(`%s; extra == %q`, req, extra) +} + +// buildMockIndex builds a MockIndex from the scenario's [packages], with wheel +// tag data declared complete so WheelTagFilter can act on it. +func buildMockIndex(t *testing.T, name string, pkgs map[string]tomlPackage) *index.MockIndex { + t.Helper() + + idx := index.NewMockIndex(name).SetWheelTagsComplete(true) + for pkgName, pkg := range pkgs { + if len(pkg.Versions) == 0 { + idx.AddPackage(pkgName) + continue + } + for verStr, v := range pkg.Versions { + reqs := append([]string(nil), v.Requires...) + var extraNames []string + for extra, extraReqs := range v.Extras { + extraNames = append(extraNames, extra) + for _, r := range extraReqs { + reqs = append(reqs, extraRequirement(r, extra)) + } + } + sort.Strings(extraNames) + + facts := versionDistFacts(v) + meta, err := index.ParseRecord(index.RawRecord{ + RequiresDist: reqs, + RequiresPython: requiresPythonOf(v), + ProvidesExtra: extraNames, + WheelTags: facts.wheelTags, + HasSdist: facts.hasSdist, + TagsCaptured: true, + }) + if err != nil { + t.Fatalf("%s %s %s: %v", name, pkgName, verStr, err) + } + idx.SetMetadata(pkgName, verStr, meta) + + // Files carry Yanked, which FilteredIndex.ExcludeYanked (wrapped on in + // runPackseScenario) reads. Populated for every version, not just a + // yanked one -- ExcludeYanked only drops a version whose files are ALL + // yanked, so an unyanked version needs at least one file recorded or it + // would look like it has none. + for _, tag := range facts.wheelTags { + idx.AddFiles(pkgName, verStr, index.DistFile{ + Filename: fmt.Sprintf("%s-%s-%s.whl", pkgName, verStr, tag), + Kind: index.DistKindWheel, + Yanked: v.Yanked, + }) + } + if facts.hasSdist { + idx.AddFiles(pkgName, verStr, index.DistFile{ + Filename: fmt.Sprintf("%s-%s.tar.gz", pkgName, verStr), + Kind: index.DistKindSDist, + Yanked: v.Yanked, + }) + } + } + } + return idx +} + +// allPackageNames returns every package name the scenario declares, in sorted +// order -- used to emulate resolver_options.prereleases (a blanket "allow +// pre-releases everywhere") through resolver.Options.AllowPrerelease, which is +// a per-package allow list. The two are equivalent exactly because no package +// outside this scenario can ever be offered. +func allPackageNames(pkgs map[string]tomlPackage) []index.PackageName { + names := make([]string, 0, len(pkgs)) + for name := range pkgs { + names = append(names, name) + } + sort.Strings(names) + out := make([]index.PackageName, len(names)) + for i, n := range names { + out[i] = index.NewPackageName(n) + } + return out +} + +// wheelTagFilter compiles a WheelTagFilter for target, or returns an error +// packsePlatform / tags.Target.Compile produced. +func wheelTagFilter(target tags.Target, label string) (*provider.WheelTagFilter, error) { + matcher, err := target.Compile() + if err != nil { + return nil, fmt.Errorf("compile target %+v: %w", target, err) + } + return &provider.WheelTagFilter{Matcher: matcher, Target: label}, nil +} diff --git a/resolver/packse_test.go b/resolver/packse_test.go new file mode 100644 index 0000000..6706e3c --- /dev/null +++ b/resolver/packse_test.go @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT + +package resolver_test + +import ( + "context" + "errors" + "fmt" + "reflect" + "testing" + + "github.com/posit-dev/go-pyresolver/index" + "github.com/posit-dev/go-pyresolver/resolver" + "github.com/posit-dev/go-python-packaging/version" +) + +// outOfScope lists every packse scenario with resolver_options.universal = +// true, keyed by its testdata/packse-relative name (no extension). These are +// NOT run: go-pyresolver resolves one concrete marker environment at a time +// (resolver.Options.Environment's doc comment), and universal +// (environment-independent) resolution is deferred by RFD 0001. +// +// Measured against the vendored corpus (testdata/packse/README.md's pinned +// commit): exactly 41 scenarios set resolver_options.universal, all of +// fork/, tag_and_markers/, and 2 each of backtracking/ and wheels/. +var outOfScope = map[string]string{ + "backtracking/wrong-backtracking-basic": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "backtracking/wrong-backtracking-indirect": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/allows-non-conflicting-non-overlapping-dependencies": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/allows-non-conflicting-repeated-dependencies": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/basic": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/conflict-in-fork": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/conflict-unsatisfiable": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/filter-sibling-dependencies": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/fork-upgrade": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/incomplete-markers": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/marker-accrue": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/marker-disjoint": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/marker-inherit": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/marker-inherit-combined": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/marker-inherit-combined-allowed": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/marker-inherit-combined-disallowed": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/marker-inherit-isolated": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/marker-inherit-transitive": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/marker-limited-inherit": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/marker-selection": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/marker-track": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/non-fork-marker-transitive": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/non-local-fork-marker-direct": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/non-local-fork-marker-transitive": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/overlapping-markers-basic": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/preferences-dependent-forking": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/preferences-dependent-forking-bistable": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/preferences-dependent-forking-conflicting": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/preferences-dependent-forking-tristable": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/remaining-universe-partitioning": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/requires-python": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/requires-python-full": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/requires-python-full-prerelease": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "fork/requires-python-patch-overlap": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "tag_and_markers/requires-python-wheels": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "tag_and_markers/unreachable-package": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "tag_and_markers/unreachable-wheels": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "tag_and_markers/virtual-package-extra-priorities": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "tag_and_markers/virtual-package-marker-priorities": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "wheels/requires-python-subset": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", + "wheels/specific-architecture": "universal resolution (resolver_options.universal); RFD 0001 defers universal resolution", +} + +// unsupportedOption lists scenarios whose resolver_options ask for something +// resolver.Options has no field for. They are not resolved at all -- the +// harness itself refuses, which IS the assertion, rather than silently +// running without the option and reporting a result that happens to match for +// the wrong reason. +var unsupportedOption = map[string]string{ + "wheels/no-binary": "resolver_options.no_binary has no resolver.Options equivalent", + "wheels/no-build": "resolver_options.no_build has no resolver.Options equivalent", + "wheels/no-wheels-no-build": "resolver_options.no_build has no resolver.Options equivalent", + "wheels/only-wheels-no-binary": "resolver_options.no_binary has no resolver.Options equivalent", +} + +// knownFail lists scenarios that DO run against the real resolver, and are +// asserted to still disagree with packse's expected outcome. Never t.Skip: +// when the underlying gap closes, the assertion below flips to +// "unexpectedly passed" and fails, which is what makes this list honest. +var knownFail = map[string]string{ + "yanked/transitive-package-only-yanked-in-range-opt-in": "FilteredIndex.ExcludeYanked drops a yanked version outright; " + + "PEP 592 (and uv) still allow it when a requirement pins it exactly, which this scenario's root does for b==1.0.0", + "yanked/transitive-yanked-and-unyanked-dependency-opt-in": "FilteredIndex.ExcludeYanked drops a yanked version outright; " + + "PEP 592 (and uv) still allow it when a requirement pins it exactly, which this scenario's root does for c==2.0.0", + + "extras/missing-extra": "go-pyresolver models name[extra] as a virtual package requiring a candidate that " + + "declares the extra, so a version that omits it is excluded rather than the extra being silently dropped; " + + "uv ignores an extra no candidate provides", + "extras/extra-does-not-exist-backtrack": "same gap as extras/missing-extra: the newest version (3.0.0) does not " + + "provide the extra, so go-pyresolver backtracks to the one that does (1.0.0) instead of dropping the extra", + + "prereleases/package-only-prereleases": "candidate.PrereleaseSet admits a pre-release only when a specifier " + + "names one or the caller opts in; it does not implement pip/uv's further fallback of admitting one when a " + + "package publishes no final release at all (see PrereleaseSet.Admits's doc comment)", + "prereleases/package-only-prereleases-boundary": "same gap as prereleases/package-only-prereleases", + "prereleases/transitive-package-only-prereleases": "same gap as prereleases/package-only-prereleases, one level " + + "down the dependency graph", + + "requires_python/python-less-than-current": "go-pyresolver's SupportsPython enforces the full Requires-Python " + + "specifier per PEP 440, including an upper bound; uv deliberately ignores an upper bound on Requires-Python", +} + +// runPackseScenario resolves s and reports whether the result matches +// expected. +func runPackseScenario(t *testing.T, s tomlScenario) (matched bool, detail string) { + t.Helper() + + py, err := scenarioPython(s) + if err != nil { + t.Fatalf("build python target: %v", err) + } + target, err := buildTarget(py, s.ResolverOptions.PythonPlatform) + if err != nil { + t.Fatalf("build tags target: %v", err) + } + env, err := buildEnvironment(target, py) + if err != nil { + t.Fatalf("build marker environment: %v", err) + } + filter, err := wheelTagFilter(target, fmt.Sprintf("cp%d%d on %s/%s", py.major, py.minor, target.OS, target.Arch)) + if err != nil { + t.Fatalf("build wheel tag filter: %v", err) + } + + idx := index.NewFilteredIndex(buildMockIndex(t, s.Name, s.Packages), index.FilterPolicy{ExcludeYanked: true}) + + opts := resolver.Options{ + Environment: env, + PythonVersion: version.MustParse(py.full), + WheelTags: filter, + } + if s.ResolverOptions.Prereleases { + opts.AllowPrerelease = allPackageNames(s.Packages) + } + + reqs := mustRequirements(t, s.Root.Requires...) + res, resolveErr := resolver.Resolve(context.Background(), reqs, idx, opts) + + switch { + case !s.Expected.Satisfiable: + if resolveErr == nil { + return false, fmt.Sprintf("expected unsatisfiable, but resolved to %v", pins(t, res)) + } + var re *resolver.ResolutionError + if !errors.As(resolveErr, &re) { + return false, fmt.Sprintf( + "expected unsatisfiable, but Resolve failed with a non-ResolutionError "+ + "(that is the harness failing, not the resolver saying unsatisfiable): %v", resolveErr) + } + return true, "" + + case len(s.Expected.Packages) > 0: + if resolveErr != nil { + return false, fmt.Sprintf("expected packages %v, but Resolve failed: %v", s.Expected.Packages, resolveErr) + } + got := pins(t, res) + if !reflect.DeepEqual(got, s.Expected.Packages) { + return false, fmt.Sprintf("Pinned = %v, want %v", got, s.Expected.Packages) + } + return true, "" + + default: + if resolveErr != nil { + return false, fmt.Sprintf("expected to resolve, but Resolve failed: %v", resolveErr) + } + return true, "" + } +} + +// TestPackse runs every non-universal vendored packse scenario against the +// real resolver.Resolve, classified per testdata/packse/README.md and the +// three lists above. See the package doc comment on this file's neighbors for +// why a scenario belongs on one list rather than another. +func TestPackse(t *testing.T) { + scenarios := loadPackseScenarios(t) + if len(scenarios) == 0 { + t.Fatal("no scenarios loaded -- testdata/packse is missing or empty") + } + + seen := make(map[string]bool, len(scenarios)) + var pass, known, unsupported, outScope int + + for _, ps := range scenarios { + name := ps.relName + s := ps.scenario + seen[name] = true + + if reason, ok := outOfScope[name]; ok { + if !s.ResolverOptions.Universal { + t.Errorf("%s: listed in outOfScope (%s) but resolver_options.universal is not set", name, reason) + } + outScope++ + continue + } + if s.ResolverOptions.Universal { + t.Errorf("%s: resolver_options.universal is set but not listed in outOfScope", name) + continue + } + + if reason, ok := unsupportedOption[name]; ok { + _ = reason + unsupported++ + continue + } + if len(s.ResolverOptions.NoBuild) > 0 || len(s.ResolverOptions.NoBinary) > 0 { + t.Errorf("%s: uses no_build/no_binary but is not listed in unsupportedOption", name) + continue + } + if s.ResolverOptions.PythonPlatform != nil { + if _, _, ok := packsePlatform(*s.ResolverOptions.PythonPlatform); !ok { + t.Errorf("%s: python_platform %q is not recognized and not listed in unsupportedOption", + name, *s.ResolverOptions.PythonPlatform) + continue + } + } + + reason, isKnownFail := knownFail[name] + t.Run(name, func(t *testing.T) { + matched, detail := runPackseScenario(t, s) + switch { + case isKnownFail && matched: + t.Errorf("%s unexpectedly passed, remove it from knownFail (reason was: %s)", name, reason) + case isKnownFail && !matched: + // Expected: still fails. Nothing to assert further. + case !isKnownFail && !matched: + t.Errorf("%s: %s", name, detail) + } + }) + + if isKnownFail { + known++ + } else { + pass++ + } + } + + for name := range knownFail { + if !seen[name] { + t.Errorf("knownFail names %q, which does not exist in testdata/packse", name) + } + } + for name := range unsupportedOption { + if !seen[name] { + t.Errorf("unsupportedOption names %q, which does not exist in testdata/packse", name) + } + } + for name := range outOfScope { + if !seen[name] { + t.Errorf("outOfScope names %q, which does not exist in testdata/packse", name) + } + } + + t.Logf("packse: %d scenarios = %d pass + %d known-fail + %d unsupported + %d out-of-scope", + len(scenarios), pass, known, unsupported, outScope) +} diff --git a/resolver/satoracle_test.go b/resolver/satoracle_test.go new file mode 100644 index 0000000..29dafe3 --- /dev/null +++ b/resolver/satoracle_test.go @@ -0,0 +1,466 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT + +package resolver_test + +import ( + "context" + "errors" + "fmt" + "sort" + "strings" + "testing" + + "github.com/crillab/gophersat/bf" + "github.com/posit-dev/go-pyresolver/candidate" + "github.com/posit-dev/go-pyresolver/index" + "github.com/posit-dev/go-pyresolver/resolver" + "github.com/posit-dev/go-python-packaging/extras" + "github.com/posit-dev/go-python-packaging/marker" + "github.com/posit-dev/go-python-packaging/requirement" + "github.com/posit-dev/go-python-packaging/tags" + "github.com/posit-dev/go-python-packaging/version" +) + +// oracleKnownFail lists scenarios where the oracle and the resolver +// deliberately disagree, on top of runPackseScenario's own knownFail. Both +// entries here implement something the resolver's matching knownFail entry +// says the resolver does not: the PEP 592 exact-pin exception for a yanked +// version. The oracle computes yanked admissibility itself (see +// exactPinAdmitsYanked), correctly this time, so it agrees with packse and +// disagrees with the resolver. +var oracleKnownFail = map[string]string{ + "yanked/transitive-package-only-yanked-in-range-opt-in": "oracle implements PEP 592's exact-pin " + + "exception for a yanked version; the resolver does not (see its own knownFail entry)", + "yanked/transitive-yanked-and-unyanked-dependency-opt-in": "oracle implements PEP 592's exact-pin " + + "exception for a yanked version; the resolver does not (see its own knownFail entry)", +} + +// oracleVersion is what the CNF encoder needs about one published version, +// derived entirely from scenario data (never from provider or candidate +// output -- see the package doc comment on this file's neighbors for why). +type oracleVersion struct { + str string + parsed version.Version + admissible bool + provides map[string]bool +} + +// exactPinAdmitsYanked reports whether some root requirement pins pkgName to +// exactly v, which is PEP 592's exception to "never offer a yanked release": +// an installer MAY still honor an explicit, exact request for it. Checked +// against the raw requirement text rather than a general specifier-shape +// analysis -- sufficient for the vendored corpus (measured: both scenarios +// that need this write it as "name==x.y.z"), and simpler than reconstructing +// "is this specifier exactly one version" from a parsed Specifiers. +func exactPinAdmitsYanked(root tomlRoot, pkgName string, v version.Version) bool { + needle := pkgName + "==" + v.String() + for _, raw := range root.Requires { + if strings.Contains(raw, needle) { + return true + } + } + return false +} + +// oracleAdmissible computes, independently of provider/candidate, whether v +// may be offered at all: Requires-Python, pre-release admission (sharing +// candidate.PrereleaseSet -- the one deliberate exception named in this +// package's mutation-proof and PR-body notes), and wheel/sdist availability. +func oracleAdmissible( + v tomlVersion, parsed version.Version, py pythonSpec, prereleases candidate.PrereleaseSet, pkgName string, matcher *tags.Matcher, +) bool { + reqPy := requiresPythonOf(v) + if reqPy != "" { + specs, err := version.NewSpecifiers(reqPy) + if err == nil && !specs.Check(version.MustParse(py.full)) { + return false + } + } + if !prereleases.Admits(index.NewPackageName(pkgName), parsed) { + return false + } + + facts := versionDistFacts(v) + hasCompatibleWheel := false + for _, raw := range facts.wheelTags { + parsedTag, err := tags.ParseTag(raw) + if err != nil { + continue + } + if matcher.IsCompatible(parsedTag) { + hasCompatibleWheel = true + break + } + } + if !hasCompatibleWheel && !facts.hasSdist { + return false + } + return true +} + +// atMostOne encodes "at most one of vars is true" as pairwise exclusions. +// packse's version counts per package are small, so the quadratic clause +// count is not a concern. +func atMostOne(vars []string) bf.Formula { + var clauses []bf.Formula + for i := 0; i < len(vars); i++ { + for j := i + 1; j < len(vars); j++ { + clauses = append(clauses, bf.Or(bf.Not(bf.Var(vars[i])), bf.Not(bf.Var(vars[j])))) + } + } + if len(clauses) == 0 { + // ⚠️ Fewer than 2 versions: no pair to exclude. Must be the explicit + // bf.True constant, NOT bf.And() with zero arguments -- gophersat's own + // nnf() simplification maps an all-true/empty conjunction to False (the + // opposite of the vacuous truth this represents), which silently turns a + // satisfiable scenario UNSAT under bf.Solve while direct Eval still + // (correctly) reads it as true. Measured against gophersat v1.4.0. + return bf.True + } + return bf.And(clauses...) +} + +// oracleModel is the CNF built for one scenario, plus what's needed to +// evaluate the resolver's own answer against it directly. +// +// vars names every variable the formula mentions. bf.Formula.Eval panics on a +// model with no binding for a variable it reaches, so resolverModel must +// supply an explicit false for everything the resolver did not pin -- vars is +// what makes that possible. +type oracleModel struct { + formula bf.Formula + vars []string +} + +func projVar(pkg, ver string) string { return pkg + "@" + ver } +func extraVar(pkg, extra, ver string) string { return pkg + "[" + extra + "]@" + ver } + +// buildOracleModel encodes s as CNF. One variable per (package, version) plus +// one per (package[extra], version); at-most-one per project; the root +// requirements; and implications of the form x(p,v) -> OR(admissible +// versions of each dependency). Extras are modeled the way go-pyresolver +// models them (a version must itself declare an extra for a request naming it +// to admit that version) so the oracle agrees with the resolver on the +// extras/ knownFail scenarios rather than adding a second, unrelated +// disagreement. +func buildOracleModel(t *testing.T, s tomlScenario, py pythonSpec, env marker.Environment, matcher *tags.Matcher) oracleModel { + t.Helper() + + prereleases := candidate.EnabledPrereleases(mustRequirements(t, s.Root.Requires...), oraclePrereleaseAllow(s)) + + versions := make(map[string][]oracleVersion, len(s.Packages)) + for pkgName, pkg := range s.Packages { + name := index.NewPackageName(pkgName).String() + for verStr, v := range pkg.Versions { + parsed, err := version.Parse(verStr) + if err != nil { + t.Fatalf("oracle %s: %s %s: bad version: %v", s.Name, pkgName, verStr, err) + } + admissible := oracleAdmissible(v, parsed, py, prereleases, name, matcher) && + (!v.Yanked || exactPinAdmitsYanked(s.Root, pkgName, parsed)) + provides := make(map[string]bool, len(v.Extras)) + for e := range v.Extras { + provides[extras.Normalize(e)] = true + } + versions[name] = append(versions[name], oracleVersion{str: verStr, parsed: parsed, admissible: admissible, provides: provides}) + } + } + + requirementFormula := func(r requirement.Requirement) bf.Formula { + if !r.Marker.Evaluate(env, nil) { + return bf.True // marker false: this edge does not apply, vacuously true + } + name := index.NewPackageName(r.Name).String() + var terms []bf.Formula + for _, vi := range versions[name] { + if !vi.admissible { + continue + } + if r.Specifiers.String() != "" && !r.Specifiers.Check(vi.parsed) { + continue + } + ok := true + for _, e := range r.Extras { + if !vi.provides[e] { + ok = false + break + } + } + if !ok { + continue + } + term := []bf.Formula{bf.Var(projVar(name, vi.str))} + for _, e := range r.Extras { + term = append(term, bf.Var(extraVar(name, e, vi.str))) + } + terms = append(terms, bf.And(term...)) + } + if len(terms) == 0 { + // No admissible candidate satisfies this requirement at all: the + // requirement is unsatisfiable. Must be the explicit bf.False + // constant, not bf.Or() with zero arguments -- see atMostOne's + // comment; gophersat's nnf() maps an empty disjunction to True, the + // opposite of what an unsatisfiable requirement needs. + return bf.False + } + return bf.Or(terms...) + } + + clauses := make([]bf.Formula, 0, len(s.Root.Requires)+len(versions)*2) + for _, raw := range s.Root.Requires { + req, err := requirement.Parse(raw) + if err != nil { + t.Fatalf("oracle %s: root requirement %q: %v", s.Name, raw, err) + } + clauses = append(clauses, requirementFormula(req)) + } + + // Sort package names so clause order (and any solver tie-breaking) does + // not depend on Go's randomized map iteration. + names := make([]string, 0, len(versions)) + for name := range versions { + names = append(names, name) + } + sort.Strings(names) + + var allVars []string + + for _, name := range names { + vis := versions[name] + sort.Slice(vis, func(i, j int) bool { return vis[i].str < vis[j].str }) + + varNames := make([]string, len(vis)) + for i, vi := range vis { + varNames[i] = projVar(name, vi.str) + } + allVars = append(allVars, varNames...) + clauses = append(clauses, atMostOne(varNames)) + + for _, vi := range vis { + if !vi.admissible { + continue + } + pkgName := originalPackageName(s.Packages, name) + v := s.Packages[pkgName].Versions[vi.str] + + var deps []bf.Formula + for _, raw := range v.Requires { + req, err := requirement.Parse(raw) + if err != nil { + t.Fatalf("oracle %s: %s %s requirement %q: %v", s.Name, name, vi.str, raw, err) + } + deps = append(deps, requirementFormula(req)) + } + if len(deps) > 0 { + clauses = append(clauses, bf.Implies(bf.Var(projVar(name, vi.str)), bf.And(deps...))) + } + + extraNames := make([]string, 0, len(v.Extras)) + for e := range v.Extras { + extraNames = append(extraNames, e) + } + sort.Strings(extraNames) + for _, e := range extraNames { + reqs := v.Extras[e] + canonExtra := extras.Normalize(e) + edeps := []bf.Formula{bf.Var(projVar(name, vi.str))} + for _, raw := range reqs { + req, err := requirement.Parse(raw) + if err != nil { + t.Fatalf("oracle %s: %s %s extra %q requirement %q: %v", s.Name, name, vi.str, e, raw, err) + } + edeps = append(edeps, requirementFormula(req)) + } + clauses = append(clauses, bf.Implies(bf.Var(extraVar(name, canonExtra, vi.str)), bf.And(edeps...))) + allVars = append(allVars, extraVar(name, canonExtra, vi.str)) + } + } + } + + return oracleModel{formula: bf.And(clauses...), vars: allVars} +} + +// originalPackageName finds the raw TOML key for a canonicalized name -- +// needed because s.Packages is keyed by the un-normalized name packse wrote, +// but the CNF is built over canonical names to line up with +// resolver.Resolution.Pinned. +func originalPackageName(pkgs map[string]tomlPackage, canonical string) string { + for raw := range pkgs { + if index.NewPackageName(raw).String() == canonical { + return raw + } + } + return canonical +} + +// oraclePrereleaseAllow mirrors runPackseScenario's resolver_options.prereleases +// emulation, so the oracle and the resolver share the same admission input. +func oraclePrereleaseAllow(s tomlScenario) []index.PackageName { + if !s.ResolverOptions.Prereleases { + return nil + } + return allPackageNames(s.Packages) +} + +// resolverModel builds the variable assignment the CNF sees for a resolved +// Resolution: true for each pinned (package, version) and each active +// (package, extra) at that version, false for every other variable the model +// mentions (bf.Formula.Eval panics on an unbound one). +func resolverModel(vars []string, res *resolver.Resolution) map[string]bool { + model := make(map[string]bool, len(vars)) + for _, name := range vars { + model[name] = false + } + for name, v := range res.Pinned { + model[projVar(name.String(), v.String())] = true + } + for name, extraList := range res.Extras { + v, ok := res.Pinned[name] + if !ok { + continue + } + for _, e := range extraList { + model[extraVar(name.String(), e, v.String())] = true + } + } + return model +} + +// oracleRow is one line of the three-way disagreement table the PR body +// quotes. +type oracleRow struct { + name, packse, resolverAns, oracleAns string +} + +// TestSATOracle cross-checks resolver.Resolve against an independent CNF +// encoding of the same scenario, built from packse's own scenario data (see +// buildOracleModel). Two directions, per scenario: +// +// - The resolver resolves => its pins satisfy the CNF (checked by +// evaluating the model directly, never by calling gophersat). +// - The resolver says unsatisfiable => gophersat also returns UNSAT. +// +// A disagreement fails the case unless the scenario is on oracleKnownFail. +func TestSATOracle(t *testing.T) { + scenarios := loadPackseScenarios(t) + if len(scenarios) == 0 { + t.Fatal("no scenarios loaded -- testdata/packse is missing or empty") + } + + var rows []oracleRow + var checked int + + for _, ps := range scenarios { + name := ps.relName + s := ps.scenario + if s.ResolverOptions.Universal { + continue // outOfScope, per TestPackse + } + if _, ok := unsupportedOption[name]; ok { + continue + } + if len(s.ResolverOptions.NoBuild) > 0 || len(s.ResolverOptions.NoBinary) > 0 { + continue + } + if s.ResolverOptions.PythonPlatform != nil { + if _, _, ok := packsePlatform(*s.ResolverOptions.PythonPlatform); !ok { + continue + } + } + checked++ + + t.Run(name, func(t *testing.T) { + py, err := scenarioPython(s) + if err != nil { + t.Fatalf("build python target: %v", err) + } + target, err := buildTarget(py, s.ResolverOptions.PythonPlatform) + if err != nil { + t.Fatalf("build tags target: %v", err) + } + env, err := buildEnvironment(target, py) + if err != nil { + t.Fatalf("build marker environment: %v", err) + } + matcher, err := target.Compile() + if err != nil { + t.Fatalf("compile target: %v", err) + } + filter, err := wheelTagFilter(target, fmt.Sprintf("cp%d%d on %s/%s", py.major, py.minor, target.OS, target.Arch)) + if err != nil { + t.Fatalf("build wheel tag filter: %v", err) + } + + idx := index.NewFilteredIndex(buildMockIndex(t, s.Name, s.Packages), index.FilterPolicy{ExcludeYanked: true}) + opts := resolver.Options{Environment: env, PythonVersion: version.MustParse(py.full), WheelTags: filter} + if s.ResolverOptions.Prereleases { + opts.AllowPrerelease = allPackageNames(s.Packages) + } + + reqs := mustRequirements(t, s.Root.Requires...) + res, resolveErr := resolver.Resolve(context.Background(), reqs, idx, opts) + + model := buildOracleModel(t, s, py, env, matcher) + oracleAssignment := bf.Solve(model.formula) + oracleSAT := oracleAssignment != nil + + var re *resolver.ResolutionError + resolverSAT := resolveErr == nil + resolverIsConflict := errors.As(resolveErr, &re) + + row := oracleRow{ + name: name, + packse: satLabel(s.Expected.Satisfiable), + resolverAns: satLabel(resolverSAT), + oracleAns: satLabel(oracleSAT), + } + if row.packse != row.resolverAns || row.packse != row.oracleAns || row.resolverAns != row.oracleAns { + rows = append(rows, row) + } + + switch { + case resolverSAT: + // Direction 1: the resolver's own pins must satisfy the CNF. + if !model.formula.Eval(resolverModel(model.vars, res)) { + t.Errorf("%s: resolver resolved to %v, but that assignment does not satisfy the oracle's CNF", name, pins(t, res)) + } + + case resolverIsConflict: + // Direction 2: the resolver's unsatisfiable must agree with gophersat. + reason, known := oracleKnownFail[name] + switch { + case known && oracleSAT: + // Expected disagreement; nothing further to assert. + case known && !oracleSAT: + t.Errorf("%s unexpectedly agreed with the resolver, remove it from oracleKnownFail (reason was: %s)", name, reason) + case !known && oracleSAT: + t.Errorf("%s: resolver says unsatisfiable, but the oracle found a model: %v", name, oracleAssignment) + } + + default: + // A harness-level error (not a ResolutionError): the oracle has + // nothing to cross-check against a failure that is not the + // resolver's own answer. + t.Fatalf("%s: Resolve failed with a non-ResolutionError: %v", name, resolveErr) + } + }) + } + + t.Logf("SAT oracle: cross-checked %d scenarios, %d disagreement(s)", checked, len(rows)) + if len(rows) > 0 { + var b strings.Builder + b.WriteString("scenario | packse | resolver | oracle\n") + b.WriteString("---|---|---|---\n") + for _, r := range rows { + fmt.Fprintf(&b, "%s | %s | %s | %s\n", r.name, r.packse, r.resolverAns, r.oracleAns) + } + t.Log(b.String()) + } +} + +func satLabel(satisfiable bool) string { + if satisfiable { + return "sat" + } + return "unsat" +} diff --git a/resolver/testdata/packse/LICENSE-APACHE b/resolver/testdata/packse/LICENSE-APACHE new file mode 100644 index 0000000..f49a4e1 --- /dev/null +++ b/resolver/testdata/packse/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/resolver/testdata/packse/LICENSE-MIT b/resolver/testdata/packse/LICENSE-MIT new file mode 100644 index 0000000..ec2236b --- /dev/null +++ b/resolver/testdata/packse/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Astral Software Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/resolver/testdata/packse/README.md b/resolver/testdata/packse/README.md new file mode 100644 index 0000000..0980563 --- /dev/null +++ b/resolver/testdata/packse/README.md @@ -0,0 +1,41 @@ +# packse scenarios + +Vendored from [astral-sh/packse](https://github.com/astral-sh/packse), commit +`18be7766b14aa3e03db37bc575959b3f4c6fbacb` (2026-08-18, after tag `0.3.59`). + +## What this is + +All 147 scenario TOML files under packse's `scenarios/` directory, copied verbatim +with their category subdirectories, plus `LICENSE-APACHE` and `LICENSE-MIT`. +`scenarios/examples/` (one `.toml`, one `.json`, one `.yaml`, not real test +scenarios) is left out. + +## How it was copied + +```sh +git clone https://github.com/astral-sh/packse /tmp/packse +git -C /tmp/packse checkout 18be7766b14aa3e03db37bc575959b3f4c6fbacb +cp -R /tmp/packse/scenarios/{backtracking,does_not_exist,excluded,extras,fork,\ +incompatible_versions,local,post,prereleases,requires_python,tag_and_markers,\ +wheels,yanked} resolver/testdata/packse/ +cp /tmp/packse/LICENSE-APACHE /tmp/packse/LICENSE-MIT resolver/testdata/packse/ +``` + +## Out of scope + +`resolver_options.universal = true` scenarios (41 of them: all 32 of `fork/`, 5 +`tag_and_markers/`, 2 `backtracking/`, 2 `wheels/`) are not run. go-pyresolver +resolves one concrete marker environment at a time (see +`resolver.Options.Environment`); universal (environment-independent) +resolution is deferred by RFD 0001. `resolver/packse_test.go`'s `outOfScope` +list names them. + +## How to refresh + +Bump the pinned commit above, re-run the copy command (all 147 files, so a +diff shows exactly what packse changed), and re-run +`go test ./resolver/ -run TestPackse -v`. A new or removed scenario fails the +"every scenario classified exactly once" check in `packse_test.go`, and a +scenario with a new `resolver_options` or per-version key fails the loader's +strict-decode check -- both by design, so update the classification lists +rather than loosening the check. diff --git a/resolver/testdata/packse/backtracking/backtrack-to-missing-package.toml b/resolver/testdata/packse/backtracking/backtrack-to-missing-package.toml new file mode 100644 index 0000000..75f08c6 --- /dev/null +++ b/resolver/testdata/packse/backtracking/backtrack-to-missing-package.toml @@ -0,0 +1,23 @@ +name = "backtrack-to-missing-package" +description = """There are two packages, `a` and `b`. All versions of `b` require a specific +version of `a`, but that version requires a package `c` that does not exist. The resolver +must backtrack through all versions of `b` and eventually fail because no solution exists.""" + +[expected] +satisfiable = false + +[root] +requires = ["a", "b"] + +[packages.a.versions] +# This version works but is incompatible with all versions of b +"2.0.0" = {} +# This version is required by b but depends on a missing package +"1.0.0" = { requires = ["c"] } + +[packages.b.versions] +"1.0.0" = { requires = ["a==1.0.0"] } +"2.0.0" = { requires = ["a==1.0.0"] } +"3.0.0" = { requires = ["a==1.0.0"] } + +# Note: package `c` is intentionally not defined (missing package) diff --git a/resolver/testdata/packse/backtracking/backtrack-with-missing-package.toml b/resolver/testdata/packse/backtracking/backtrack-with-missing-package.toml new file mode 100644 index 0000000..e1413e3 --- /dev/null +++ b/resolver/testdata/packse/backtracking/backtrack-with-missing-package.toml @@ -0,0 +1,27 @@ +name = "backtrack-with-missing-package" +description = """There are two packages, `a` and `b`. The latest version of `b` requires +a specific version of `a`. The older version of `b` requires a package `c` that does not +exist. The resolver should backtrack on `a` (not `b`) to find a solution without needing +to try `b==1.0.0` which would fail due to the missing package.""" + +[expected] +satisfiable = true + +[expected.packages] +a = "1.0.0" +b = "2.0.0" + +[root] +requires = ["a", "b"] + +[packages.a.versions] +"1.0.0" = {} +"2.0.0" = {} + +[packages.b.versions] +# Old version requires a missing package - resolver should not need this +"1.0.0" = { requires = ["c"] } +# New version requires specific a version - should work with backtracking +"2.0.0" = { requires = ["a==1.0.0"] } + +# Note: package `c` is intentionally not defined (missing package) diff --git a/resolver/testdata/packse/backtracking/wrong-backtracking-basic.toml b/resolver/testdata/packse/backtracking/wrong-backtracking-basic.toml new file mode 100644 index 0000000..f1705e5 --- /dev/null +++ b/resolver/testdata/packse/backtracking/wrong-backtracking-basic.toml @@ -0,0 +1,38 @@ +name = "wrong-backtracking-basic" +description = """There are two packages, `a` and `b`. We select `a` with `a==2.0.0` first, and then `b`, but `a==2.0.0` conflicts with all new versions of `b`, so we backtrack through versions of `b`. + +We need to detect this conflict and prioritize `b` over `a` instead of backtracking down to the too old version of `b==1.0.0` that doesn't depend on `a` anymore.""" + +[expected] +satisfiable = true + +[resolver_options] +universal = true + +[expected.packages] +a = "1.0.0" +b = "2.0.9" + +[root] +requires = ["a", "b"] + +[packages.a.versions] +"1.0.0" = {} +"2.0.0" = {} + +[packages.too-old.versions] +"1.0.0" = {} + +[packages.b.versions] +# We must not backtrack to this very old versions +"1.0.0" = { requires = ["too-old"] } +"2.0.0" = { requires = ["a==1.0.0"] } +"2.0.1" = { requires = ["a==1.0.0"] } +"2.0.2" = { requires = ["a==1.0.0"] } +"2.0.3" = { requires = ["a==1.0.0"] } +"2.0.4" = { requires = ["a==1.0.0"] } +"2.0.5" = { requires = ["a==1.0.0"] } +"2.0.6" = { requires = ["a==1.0.0"] } +"2.0.7" = { requires = ["a==1.0.0"] } +"2.0.8" = { requires = ["a==1.0.0"] } +"2.0.9" = { requires = ["a==1.0.0"] } diff --git a/resolver/testdata/packse/backtracking/wrong-backtracking-indirect.toml b/resolver/testdata/packse/backtracking/wrong-backtracking-indirect.toml new file mode 100644 index 0000000..a3f1297 --- /dev/null +++ b/resolver/testdata/packse/backtracking/wrong-backtracking-indirect.toml @@ -0,0 +1,45 @@ +name = "wrong-backtracking-indirect" +description = """There are three packages, `a`, `b` and `b-inner`. Unlike wrong-backtracking-basic, `b` depends on `b-inner` and `a` and `b-inner` conflict, to add a layer of indirection. + +We select `a` with `a==2.0.0` first, then `b`, and then `b-inner`, but `a==2.0.0` conflicts with all new versions of `b-inner`, so we backtrack through versions of `b-inner`. + +We need to detect this conflict and prioritize `b` and `b-inner` over `a` instead of backtracking down to the too old version of `b-inner==1.0.0` that doesn't depend on `a` anymore.""" + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +# TODO: https://github.com/astral-sh/uv/issues/12060 +# [expected.packages] +# a = "1.0.0" +# b = "1.0.0" +# b-inner = "2.0.9" + +[root] +requires = ["a", "b"] + +[packages.a.versions] +"1.0.0" = {} +"2.0.0" = {} + +[packages.too-old.versions] +"1.0.0" = {} + +[packages.b.versions] +"1.0.0" = { requires = ["b-inner"] } + +[packages.b-inner.versions] +# We must not backtrack to this very old versions +"1.0.0" = { requires = ["too-old"] } +"2.0.0" = { requires = ["a==1.0.0"] } +"2.0.1" = { requires = ["a==1.0.0"] } +"2.0.2" = { requires = ["a==1.0.0"] } +"2.0.3" = { requires = ["a==1.0.0"] } +"2.0.4" = { requires = ["a==1.0.0"] } +"2.0.5" = { requires = ["a==1.0.0"] } +"2.0.6" = { requires = ["a==1.0.0"] } +"2.0.7" = { requires = ["a==1.0.0"] } +"2.0.8" = { requires = ["a==1.0.0"] } +"2.0.9" = { requires = ["a==1.0.0"] } diff --git a/resolver/testdata/packse/does_not_exist/requires-exact-version-does-not-exist.toml b/resolver/testdata/packse/does_not_exist/requires-exact-version-does-not-exist.toml new file mode 100644 index 0000000..fcaceaf --- /dev/null +++ b/resolver/testdata/packse/does_not_exist/requires-exact-version-does-not-exist.toml @@ -0,0 +1,10 @@ +name = "requires-exact-version-does-not-exist" +description = "The user requires an exact version of package `a` but only other versions exist" + +[root] +requires = ["a==2.0.0"] + +[expected] +satisfiable = false + +[packages.a.versions."1.0.0"] diff --git a/resolver/testdata/packse/does_not_exist/requires-greater-version-does-not-exist.toml b/resolver/testdata/packse/does_not_exist/requires-greater-version-does-not-exist.toml new file mode 100644 index 0000000..ba1db63 --- /dev/null +++ b/resolver/testdata/packse/does_not_exist/requires-greater-version-does-not-exist.toml @@ -0,0 +1,12 @@ +name = "requires-greater-version-does-not-exist" +description = "The user requires a version of `a` greater than `1.0.0` but only smaller or equal versions exist" + +[root] +requires = ["a>1.0.0"] + +[expected] +satisfiable = false + +[packages.a.versions."0.1.0"] + +[packages.a.versions."1.0.0"] diff --git a/resolver/testdata/packse/does_not_exist/requires-less-version-does-not-exist.toml b/resolver/testdata/packse/does_not_exist/requires-less-version-does-not-exist.toml new file mode 100644 index 0000000..b690544 --- /dev/null +++ b/resolver/testdata/packse/does_not_exist/requires-less-version-does-not-exist.toml @@ -0,0 +1,14 @@ +name = "requires-less-version-does-not-exist" +description = "The user requires a version of `a` less than `1.0.0` but only larger versions exist" + +[root] +requires = ["a<2.0.0"] + +[expected] +satisfiable = false + +[packages.a.versions."2.0.0"] + +[packages.a.versions."3.0.0"] + +[packages.a.versions."4.0.0"] diff --git a/resolver/testdata/packse/does_not_exist/requires-package-does-not-exist.toml b/resolver/testdata/packse/does_not_exist/requires-package-does-not-exist.toml new file mode 100644 index 0000000..001ae7b --- /dev/null +++ b/resolver/testdata/packse/does_not_exist/requires-package-does-not-exist.toml @@ -0,0 +1,10 @@ +name = "requires-package-does-not-exist" +description = "The user requires any version of package `a` which does not exist." + +[root] +requires = ["a"] + +[packages] + +[expected] +satisfiable = false diff --git a/resolver/testdata/packse/does_not_exist/transitive-requires-package-does-not-exist.toml b/resolver/testdata/packse/does_not_exist/transitive-requires-package-does-not-exist.toml new file mode 100644 index 0000000..036b0a0 --- /dev/null +++ b/resolver/testdata/packse/does_not_exist/transitive-requires-package-does-not-exist.toml @@ -0,0 +1,11 @@ +name = "transitive-requires-package-does-not-exist" +description = "The user requires package `a` but `a` requires package `b` which does not exist" + +[root] +requires = ["a"] + +[expected] +satisfiable = false + +[packages.a.versions."1.0.0"] +requires = ["b"] diff --git a/resolver/testdata/packse/excluded/dependency-excludes-non-contiguous-range-of-compatible-versions.toml b/resolver/testdata/packse/excluded/dependency-excludes-non-contiguous-range-of-compatible-versions.toml new file mode 100644 index 0000000..f51a230 --- /dev/null +++ b/resolver/testdata/packse/excluded/dependency-excludes-non-contiguous-range-of-compatible-versions.toml @@ -0,0 +1,42 @@ +name = "dependency-excludes-non-contiguous-range-of-compatible-versions" +description = "There is a non-contiguous range of compatible versions for the requested package `a`, but another dependency `c` excludes the range. This is the same as `dependency-excludes-range-of-compatible-versions` but some of the versions of `a` are incompatible for another reason e.g. dependency on non-existent package `d`." + +[root] +requires = ["a", "b>=2.0.0,<3.0.0", "c"] + +[expected] +satisfiable = false +explanation = "Only the `2.x` versions of `a` are available since `a==1.0.0` and `a==3.0.0` require incompatible versions of `b`, but all available versions of `c` exclude that range of `a` so resolution fails." + +[packages.a.versions."1.0.0"] +requires = ["b==1.0.0"] + +[packages.a.versions."2.0.0"] +requires = ["b==2.0.0"] + +[packages.a.versions."2.1.0"] +requires = ["b==2.0.0", "d"] + +[packages.a.versions."2.2.0"] +requires = ["b==2.0.0"] + +[packages.a.versions."2.3.0"] +requires = ["b==2.0.0", "d"] + +[packages.a.versions."2.4.0"] +requires = ["b==2.0.0"] + +[packages.a.versions."3.0.0"] +requires = ["b==3.0.0"] + +[packages.b.versions."1.0.0"] + +[packages.b.versions."2.0.0"] + +[packages.b.versions."3.0.0"] + +[packages.c.versions."1.0.0"] +requires = ["a<2.0.0"] + +[packages.c.versions."2.0.0"] +requires = ["a>=3.0.0"] diff --git a/resolver/testdata/packse/excluded/dependency-excludes-range-of-compatible-versions.toml b/resolver/testdata/packse/excluded/dependency-excludes-range-of-compatible-versions.toml new file mode 100644 index 0000000..1fe20d6 --- /dev/null +++ b/resolver/testdata/packse/excluded/dependency-excludes-range-of-compatible-versions.toml @@ -0,0 +1,39 @@ +name = "dependency-excludes-range-of-compatible-versions" +description = "There is a range of compatible versions for the requested package `a`, but another dependency `c` excludes that range." + +[root] +requires = ["a", "b>=2.0.0,<3.0.0", "c"] + +[expected] +satisfiable = false +explanation = "Only the `2.x` versions of `a` are available since `a==1.0.0` and `a==3.0.0` require incompatible versions of `b`, but all available versions of `c` exclude that range of `a` so resolution fails." + +[packages.a.versions."1.0.0"] +requires = ["b==1.0.0"] + +[packages.a.versions."2.0.0"] +requires = ["b==2.0.0"] + +[packages.a.versions."2.1.0"] +requires = ["b==2.0.0"] + +[packages.a.versions."2.2.0"] +requires = ["b==2.0.0"] + +[packages.a.versions."2.3.0"] +requires = ["b==2.0.0"] + +[packages.a.versions."3.0.0"] +requires = ["b==3.0.0"] + +[packages.b.versions."1.0.0"] + +[packages.b.versions."2.0.0"] + +[packages.b.versions."3.0.0"] + +[packages.c.versions."1.0.0"] +requires = ["a<2.0.0"] + +[packages.c.versions."2.0.0"] +requires = ["a>=3.0.0"] diff --git a/resolver/testdata/packse/excluded/excluded-only-compatible-version.toml b/resolver/testdata/packse/excluded/excluded-only-compatible-version.toml new file mode 100644 index 0000000..64b45e9 --- /dev/null +++ b/resolver/testdata/packse/excluded/excluded-only-compatible-version.toml @@ -0,0 +1,24 @@ +name = "excluded-only-compatible-version" +description = "Only one version of the requested package `a` is compatible, but the user has banned that version." + +[root] +requires = ["a!=2.0.0", "b>=2.0.0,<3.0.0"] + +[expected] +satisfiable = false +explanation = "Only `a==1.2.0` is available since `a==1.0.0` and `a==3.0.0` require incompatible versions of `b`. The user has excluded that version of `a` so resolution fails." + +[packages.a.versions."1.0.0"] +requires = ["b==1.0.0"] + +[packages.a.versions."2.0.0"] +requires = ["b==2.0.0"] + +[packages.a.versions."3.0.0"] +requires = ["b==3.0.0"] + +[packages.b.versions."1.0.0"] + +[packages.b.versions."2.0.0"] + +[packages.b.versions."3.0.0"] diff --git a/resolver/testdata/packse/excluded/excluded-only-version.toml b/resolver/testdata/packse/excluded/excluded-only-version.toml new file mode 100644 index 0000000..bcda827 --- /dev/null +++ b/resolver/testdata/packse/excluded/excluded-only-version.toml @@ -0,0 +1,11 @@ +name = "excluded-only-version" +description = "Only one version of the requested package is available, but the user has banned that version." + +[root] +requires = ["a!=1.0.0"] + +[expected] +satisfiable = false +explanation = "Only `a==1.0.0` is available but the user excluded it." + +[packages.a.versions."1.0.0"] diff --git a/resolver/testdata/packse/extras/all-extras-required.toml b/resolver/testdata/packse/extras/all-extras-required.toml new file mode 100644 index 0000000..013edf7 --- /dev/null +++ b/resolver/testdata/packse/extras/all-extras-required.toml @@ -0,0 +1,22 @@ +name = "all-extras-required" +description = "Multiple optional dependencies are requested for the package via an 'all' extra." + +[root] +requires = ["a[all]"] + +[expected] +satisfiable = true + +[expected.packages] +a = "1.0.0" +b = "1.0.0" +c = "1.0.0" + +[packages.b.versions."1.0.0"] + +[packages.c.versions."1.0.0"] + +[packages.a.versions."1.0.0".extras] +all = ["a[extra_b]", "a[extra_c]"] +extra_b = ["b"] +extra_c = ["c"] diff --git a/resolver/testdata/packse/extras/extra-does-not-exist-backtrack.toml b/resolver/testdata/packse/extras/extra-does-not-exist-backtrack.toml new file mode 100644 index 0000000..172aa81 --- /dev/null +++ b/resolver/testdata/packse/extras/extra-does-not-exist-backtrack.toml @@ -0,0 +1,21 @@ +name = "extra-does-not-exist-backtrack" +description = "Optional dependencies are requested for the package, the extra is only available on an older version." + +[root] +requires = ["a[extra]"] + +[expected] +satisfiable = true +explanation = "The resolver should not backtrack to `a==1.0.0` because missing extras are allowed during resolution. `b` should not be installed." + +[expected.packages] +a = "3.0.0" + +[packages.a.versions."2.0.0"] + +[packages.a.versions."3.0.0"] + +[packages.b.versions."1.0.0"] + +[packages.a.versions."1.0.0".extras] +extra = ["b==1.0.0"] diff --git a/resolver/testdata/packse/extras/extra-incompatible-with-extra-not-requested.toml b/resolver/testdata/packse/extras/extra-incompatible-with-extra-not-requested.toml new file mode 100644 index 0000000..915dd4d --- /dev/null +++ b/resolver/testdata/packse/extras/extra-incompatible-with-extra-not-requested.toml @@ -0,0 +1,21 @@ +name = "extra-incompatible-with-extra-not-requested" +description = "One of two incompatible optional dependencies are requested for the package." + +[root] +requires = ["a[extra_c]"] + +[expected] +satisfiable = true +explanation = "Because the user does not request both extras, it is okay that one is incompatible with the other." + +[expected.packages] +a = "1.0.0" +b = "2.0.0" + +[packages.b.versions."1.0.0"] + +[packages.b.versions."2.0.0"] + +[packages.a.versions."1.0.0".extras] +extra_b = ["b==1.0.0"] +extra_c = ["b==2.0.0"] diff --git a/resolver/testdata/packse/extras/extra-incompatible-with-extra.toml b/resolver/testdata/packse/extras/extra-incompatible-with-extra.toml new file mode 100644 index 0000000..14ad87c --- /dev/null +++ b/resolver/testdata/packse/extras/extra-incompatible-with-extra.toml @@ -0,0 +1,17 @@ +name = "extra-incompatible-with-extra" +description = "Multiple optional dependencies are requested for the package, but they have conflicting requirements with each other." + +[root] +requires = ["a[extra_b,extra_c]"] + +[expected] +explanation = "Because both `extra_b` and `extra_c` are requested and they require incompatible versions of `b`, `a` cannot be installed." +satisfiable = false + +[packages.b.versions."1.0.0"] + +[packages.b.versions."2.0.0"] + +[packages.a.versions."1.0.0".extras] +extra_b = ["b==1.0.0"] +extra_c = ["b==2.0.0"] diff --git a/resolver/testdata/packse/extras/extra-incompatible-with-root.toml b/resolver/testdata/packse/extras/extra-incompatible-with-root.toml new file mode 100644 index 0000000..719960d --- /dev/null +++ b/resolver/testdata/packse/extras/extra-incompatible-with-root.toml @@ -0,0 +1,16 @@ +name = "extra-incompatible-with-root" +description = "Optional dependencies are requested for the package, but the extra is not compatible with other requested versions." + +[root] +requires = ["a[extra]", "b==2.0.0"] + +[expected] +explanation = "Because the user requested `b==2.0.0` but the requested extra requires `b==1.0.0`, the dependencies cannot be satisfied." +satisfiable = false + +[packages.b.versions."1.0.0"] + +[packages.b.versions."2.0.0"] + +[packages.a.versions."1.0.0".extras] +extra = ["b==1.0.0"] diff --git a/resolver/testdata/packse/extras/extra-required.toml b/resolver/testdata/packse/extras/extra-required.toml new file mode 100644 index 0000000..9094d18 --- /dev/null +++ b/resolver/testdata/packse/extras/extra-required.toml @@ -0,0 +1,17 @@ +name = "extra-required" +description = "Optional dependencies are requested for the package." + +[root] +requires = ["a[extra]"] + +[expected] +satisfiable = true + +[expected.packages] +a = "1.0.0" +b = "1.0.0" + +[packages.b.versions."1.0.0"] + +[packages.a.versions."1.0.0".extras] +extra = ["b"] diff --git a/resolver/testdata/packse/extras/missing-extra.toml b/resolver/testdata/packse/extras/missing-extra.toml new file mode 100644 index 0000000..42f065d --- /dev/null +++ b/resolver/testdata/packse/extras/missing-extra.toml @@ -0,0 +1,14 @@ +name = "missing-extra" +description = "Optional dependencies are requested for the package, but the extra does not exist." + +[root] +requires = ["a[extra]"] + +[expected] +satisfiable = true +explanation = "Missing extras are ignored during resolution." + +[expected.packages] +a = "1.0.0" + +[packages.a.versions."1.0.0"] diff --git a/resolver/testdata/packse/extras/multiple-extras-required.toml b/resolver/testdata/packse/extras/multiple-extras-required.toml new file mode 100644 index 0000000..5d39616 --- /dev/null +++ b/resolver/testdata/packse/extras/multiple-extras-required.toml @@ -0,0 +1,21 @@ +name = "multiple-extras-required" +description = "Multiple optional dependencies are requested for the package." + +[root] +requires = ["a[extra_b,extra_c]"] + +[expected] +satisfiable = true + +[expected.packages] +a = "1.0.0" +b = "1.0.0" +c = "1.0.0" + +[packages.b.versions."1.0.0"] + +[packages.c.versions."1.0.0"] + +[packages.a.versions."1.0.0".extras] +extra_b = ["b"] +extra_c = ["c"] diff --git a/resolver/testdata/packse/fork/allows-non-conflicting-non-overlapping-dependencies.toml b/resolver/testdata/packse/fork/allows-non-conflicting-non-overlapping-dependencies.toml new file mode 100644 index 0000000..12bc128 --- /dev/null +++ b/resolver/testdata/packse/fork/allows-non-conflicting-non-overlapping-dependencies.toml @@ -0,0 +1,24 @@ +name = "fork-allows-non-conflicting-non-overlapping-dependencies" +description = ''' +This test ensures that multiple non-conflicting but also +non-overlapping dependency specifications with the same package name +are allowed and supported. + +At time of writing, this provokes a fork in the resolver, but it +arguably shouldn't since the requirements themselves do not conflict +with one another. However, this does impact resolution. Namely, it +leaves the `a>=1` fork free to choose `a==2.0.0` since it behaves as if +the `a<2` constraint doesn't exist. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = ["a>=1 ; sys_platform == 'linux'", "a<2 ; sys_platform == 'darwin'"] + +[packages.a.versions."1.0.0"] +[packages.a.versions."2.0.0"] diff --git a/resolver/testdata/packse/fork/allows-non-conflicting-repeated-dependencies.toml b/resolver/testdata/packse/fork/allows-non-conflicting-repeated-dependencies.toml new file mode 100644 index 0000000..284a637 --- /dev/null +++ b/resolver/testdata/packse/fork/allows-non-conflicting-repeated-dependencies.toml @@ -0,0 +1,26 @@ +name = "fork-allows-non-conflicting-repeated-dependencies" +description = ''' +This test ensures that multiple non-conflicting dependency +specifications with the same package name are allowed and supported. + +This test exists because the universal resolver forks itself based on +duplicate dependency specifications by looking at package name. So at +first glance, a case like this could perhaps cause an errant fork. +While it's difficult to test for "does not create a fork" (at time of +writing, the implementation does not fork), we can at least check that +this case is handled correctly without issue. Namely, forking should +only occur when there are duplicate dependency specifications with +disjoint marker expressions. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = ["a>=1", "a<2"] + +[packages.a.versions."1.0.0"] +[packages.a.versions."2.0.0"] diff --git a/resolver/testdata/packse/fork/basic.toml b/resolver/testdata/packse/fork/basic.toml new file mode 100644 index 0000000..886cd54 --- /dev/null +++ b/resolver/testdata/packse/fork/basic.toml @@ -0,0 +1,17 @@ +name = "fork-basic" +description = ''' +An extremely basic test of universal resolution. In this case, the resolution +should contain two distinct versions of `a` depending on `sys_platform`. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = ["a>=2 ; sys_platform == 'linux'", "a<2 ; sys_platform == 'darwin'"] + +[packages.a.versions."1.0.0"] +[packages.a.versions."2.0.0"] diff --git a/resolver/testdata/packse/fork/conflict-in-fork.toml b/resolver/testdata/packse/fork/conflict-in-fork.toml new file mode 100644 index 0000000..cbaed09 --- /dev/null +++ b/resolver/testdata/packse/fork/conflict-in-fork.toml @@ -0,0 +1,26 @@ +name = "conflict-in-fork" +description = ''' +We have a conflict after forking. This scenario exists to test the error message. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = false + +[root] +requires = ["a>=2 ; sys_platform == 'os1'", "a<2 ; sys_platform == 'os2'"] + +[packages.a.versions."1.0.0"] +requires = ["b", "c"] +[packages.a.versions."2.0.0"] + +[packages.b.versions."1.0.0"] +requires = ["d==1"] + +[packages.c.versions."1.0.0"] +requires = ["d==2"] + +[packages.d.versions."1.0.0"] +[packages.d.versions."2.0.0"] diff --git a/resolver/testdata/packse/fork/conflict-unsatisfiable.toml b/resolver/testdata/packse/fork/conflict-unsatisfiable.toml new file mode 100644 index 0000000..d1f8bd5 --- /dev/null +++ b/resolver/testdata/packse/fork/conflict-unsatisfiable.toml @@ -0,0 +1,23 @@ +name = "fork-conflict-unsatisfiable" +description = ''' +This test ensures that conflicting dependency specifications lead to an +unsatisfiable result. + +In particular, this is a case that should not fork even though there +are conflicting requirements because their marker expressions are +overlapping. (Well, there aren't any marker expressions here, which +means they are both unconditional.) +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = false + +[root] +requires = ["a>=2", "a<2"] + +[packages.a.versions."1.0.0"] +[packages.a.versions."2.0.0"] +[packages.a.versions."3.0.0"] diff --git a/resolver/testdata/packse/fork/filter-sibling-dependencies.toml b/resolver/testdata/packse/fork/filter-sibling-dependencies.toml new file mode 100644 index 0000000..eede687 --- /dev/null +++ b/resolver/testdata/packse/fork/filter-sibling-dependencies.toml @@ -0,0 +1,39 @@ +name = "fork-filter-sibling-dependencies" +description = ''' +This tests that sibling dependencies of a package that provokes a +fork are correctly filtered out of forks where they are otherwise +impossible. + +In this case, a previous version of the universal resolver would +include both `b` and `c` in *both* of the forks produced by the +conflicting dependency specifications on `a`. This in turn led to +transitive dependency specifications on both `d==1.0.0` and `d==2.0.0`. +Since the universal resolver only forks based on local conditions, this +led to a failed resolution. + +The correct thing to do here is to ensure that `b` is only part of the +`a==4.4.0` fork and `c` is only par of the `a==4.3.0` fork. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = [ + "a==4.4.0 ; sys_platform == 'linux'", + "a==4.3.0 ; sys_platform == 'darwin'", + "b==1.0.0 ; sys_platform == 'linux'", + "c==1.0.0 ; sys_platform == 'darwin'", +] + +[packages.a.versions."4.3.0"] +[packages.a.versions."4.4.0"] +[packages.b.versions."1.0.0"] +requires = ["d==1.0.0"] +[packages.c.versions."1.0.0"] +requires = ["d==2.0.0"] +[packages.d.versions."1.0.0"] +[packages.d.versions."2.0.0"] diff --git a/resolver/testdata/packse/fork/fork-upgrade.toml b/resolver/testdata/packse/fork/fork-upgrade.toml new file mode 100644 index 0000000..7cdc16f --- /dev/null +++ b/resolver/testdata/packse/fork/fork-upgrade.toml @@ -0,0 +1,28 @@ +name = "fork-upgrade" +description = ''' +This test checks that we discard fork markers when using `--upgrade`. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = ["foo"] + +[packages.foo.versions."1.0.0"] +requires = [ + # Provoke a fork + "bar==1; sys_platform == 'linux'", + "bar==2; sys_platform != 'linux'", +] +[packages.foo.versions."2.0.0"] +requires = [ + # No fork + "bar==2", +] + +[packages.bar.versions."1.0.0"] +[packages.bar.versions."2.0.0"] diff --git a/resolver/testdata/packse/fork/incomplete-markers.toml b/resolver/testdata/packse/fork/incomplete-markers.toml new file mode 100644 index 0000000..51124ab --- /dev/null +++ b/resolver/testdata/packse/fork/incomplete-markers.toml @@ -0,0 +1,29 @@ +name = "fork-incomplete-markers" +description = ''' +The root cause the resolver to fork over `a`, but the markers on the variant +of `a` don't cover the entire marker space, they are missing Python 3.13. +Later, we have a dependency this very hole, which we still need to select, +instead of having two forks around but without Python 3.13 and omitting +`c` from the solution. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = [ + "a==1; python_version < '3.13'", + "a==2; python_version >= '3.14'", + "b", +] + +[packages.a.versions."1.0.0"] +[packages.a.versions."2.0.0"] + +[packages.b.versions."1.0.0"] +requires = ["c; python_version == '3.13'"] + +[packages.c.versions."1.0.0"] diff --git a/resolver/testdata/packse/fork/marker-accrue.toml b/resolver/testdata/packse/fork/marker-accrue.toml new file mode 100644 index 0000000..c2b3503 --- /dev/null +++ b/resolver/testdata/packse/fork/marker-accrue.toml @@ -0,0 +1,28 @@ +name = "fork-marker-accrue" +description = ''' +This is actually a non-forking test case that tests the tracking of marker +expressions in general. In this case, the dependency on `c` should have its +marker expressions automatically combined. In this case, it's `linux OR +darwin`, even though `linux OR darwin` doesn't actually appear verbatim as a +marker expression for any dependency on `c`. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = [ + "a==1.0.0 ; implementation_name == 'cpython'", + "b==1.0.0 ; implementation_name == 'pypy'", +] + +[packages.a.versions."1.0.0"] +requires = ["c==1.0.0 ; sys_platform == 'linux'"] + +[packages.b.versions."1.0.0"] +requires = ["c==1.0.0 ; sys_platform == 'darwin'"] + +[packages.c.versions."1.0.0"] diff --git a/resolver/testdata/packse/fork/marker-disjoint.toml b/resolver/testdata/packse/fork/marker-disjoint.toml new file mode 100644 index 0000000..b9a5421 --- /dev/null +++ b/resolver/testdata/packse/fork/marker-disjoint.toml @@ -0,0 +1,26 @@ +name = "fork-marker-disjoint" +description = ''' +A basic test that ensures, at least in this one basic case, that forking in +universal resolution happens only when the corresponding marker expressions are +completely disjoint. Here, we provide two completely incompatible dependency +specifications with equivalent markers. Thus, they are trivially not disjoint, +and resolution should fail. + +NOTE: This acts a regression test for the initial version of universal +resolution that would fork whenever a package was repeated in the list of +dependency specifications. So previously, this would produce a resolution with +both `1.0.0` and `2.0.0` of `a`. But of course, the correct behavior is to fail +resolving. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = false + +[root] +requires = ["a>=2 ; sys_platform == 'linux'", "a<2 ; sys_platform == 'linux'"] + +[packages.a.versions."1.0.0"] +[packages.a.versions."2.0.0"] diff --git a/resolver/testdata/packse/fork/marker-inherit-combined-allowed.toml b/resolver/testdata/packse/fork/marker-inherit-combined-allowed.toml new file mode 100644 index 0000000..3492a81 --- /dev/null +++ b/resolver/testdata/packse/fork/marker-inherit-combined-allowed.toml @@ -0,0 +1,31 @@ +name = "fork-marker-inherit-combined-allowed" +description = ''' +This test builds on `fork-marker-inherit-combined`. Namely, we add +`or implementation_name == 'pypy'` to the dependency on `c`. While +`sys_platform == 'linux'` cannot be true because of the first fork, +the second fork which includes `b==1.0.0` happens precisely when +`implementation_name == 'pypy'`. So in this case, `c` should be +included. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = ["a>=2 ; sys_platform == 'linux'", "a<2 ; sys_platform == 'darwin'"] + +[packages.a.versions."1.0.0"] +requires = [ + "b>=2 ; implementation_name == 'cpython'", + "b<2 ; implementation_name == 'pypy'", +] +[packages.a.versions."2.0.0"] + +[packages.b.versions."1.0.0"] +requires = ["c ; sys_platform == 'linux' or implementation_name == 'pypy'"] +[packages.b.versions."2.0.0"] + +[packages.c.versions."1.0.0"] diff --git a/resolver/testdata/packse/fork/marker-inherit-combined-disallowed.toml b/resolver/testdata/packse/fork/marker-inherit-combined-disallowed.toml new file mode 100644 index 0000000..229d8c9 --- /dev/null +++ b/resolver/testdata/packse/fork/marker-inherit-combined-disallowed.toml @@ -0,0 +1,32 @@ +name = "fork-marker-inherit-combined-disallowed" +description = ''' +This test builds on `fork-marker-inherit-combined`. Namely, we add +`or implementation_name == 'cpython'` to the dependency on `c`. +While `sys_platform == 'linux'` cannot be true because of the first +fork, the second fork which includes `b==1.0.0` happens precisely +when `implementation_name == 'pypy'`, which is *also* disjoint with +`implementation_name == 'cpython'`. Therefore, `c` should not be +included here. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = ["a>=2 ; sys_platform == 'linux'", "a<2 ; sys_platform == 'darwin'"] + +[packages.a.versions."1.0.0"] +requires = [ + "b>=2 ; implementation_name == 'cpython'", + "b<2 ; implementation_name == 'pypy'", +] +[packages.a.versions."2.0.0"] + +[packages.b.versions."1.0.0"] +requires = ["c ; sys_platform == 'linux' or implementation_name == 'cpython'"] +[packages.b.versions."2.0.0"] + +[packages.c.versions."1.0.0"] diff --git a/resolver/testdata/packse/fork/marker-inherit-combined.toml b/resolver/testdata/packse/fork/marker-inherit-combined.toml new file mode 100644 index 0000000..e104f17 --- /dev/null +++ b/resolver/testdata/packse/fork/marker-inherit-combined.toml @@ -0,0 +1,33 @@ +name = "fork-marker-inherit-combined" +description = ''' +In this test, we check that marker expressions which provoke a fork +are carried through to subsequent forks. Here, the `a>=2` and `a<2` +dependency specifications create a fork, and then the `a<2` fork leads +to `a==1.0.0` with dependency specifications on `b>=2` and `b<2` that +provoke yet another fork. Finally, in the `b<2` fork, a dependency on +`c` is introduced whose marker expression is disjoint with the marker +expression that provoked the *first* fork. Therefore, `c` should be +entirely excluded from the resolution. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = ["a>=2 ; sys_platform == 'linux'", "a<2 ; sys_platform == 'darwin'"] + +[packages.a.versions."1.0.0"] +requires = [ + "b>=2 ; implementation_name == 'cpython'", + "b<2 ; implementation_name == 'pypy'", +] +[packages.a.versions."2.0.0"] + +[packages.b.versions."1.0.0"] +requires = ["c ; sys_platform == 'linux'"] +[packages.b.versions."2.0.0"] + +[packages.c.versions."1.0.0"] diff --git a/resolver/testdata/packse/fork/marker-inherit-isolated.toml b/resolver/testdata/packse/fork/marker-inherit-isolated.toml new file mode 100644 index 0000000..677407a --- /dev/null +++ b/resolver/testdata/packse/fork/marker-inherit-isolated.toml @@ -0,0 +1,25 @@ +name = "fork-marker-inherit-isolated" +description = ''' +This is like `fork-marker-inherit`, but where both `a>=2` and `a<2` +have a conditional dependency on `b`. For `a>=2`, the conditional +dependency on `b` has overlap with the `a>=2` marker expression, and +thus, `b` should be included *only* in the dependencies for `a==2.0.0`. +As with `fork-marker-inherit`, the `a<2` path should exclude `b==1.0.0` +since their marker expressions are disjoint. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = ["a>=2 ; sys_platform == 'linux'", "a<2 ; sys_platform == 'darwin'"] + +[packages.a.versions."1.0.0"] +requires = ["b ; sys_platform == 'linux'"] +[packages.a.versions."2.0.0"] +requires = ["b ; sys_platform == 'linux'"] + +[packages.b.versions."1.0.0"] diff --git a/resolver/testdata/packse/fork/marker-inherit-transitive.toml b/resolver/testdata/packse/fork/marker-inherit-transitive.toml new file mode 100644 index 0000000..1ed3334 --- /dev/null +++ b/resolver/testdata/packse/fork/marker-inherit-transitive.toml @@ -0,0 +1,30 @@ +name = "fork-marker-inherit-transitive" +description = ''' +This is like `fork-marker-inherit`, but tests that the marker +expressions that provoke a fork are carried transitively through the +dependency graph. In this case, `a<2 -> b -> c -> d`, but where the +last dependency on `d` requires a marker expression that is disjoint +with the initial `a<2` dependency. Therefore, it ought to be completely +excluded from the resolution. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = ["a>=2 ; sys_platform == 'linux'", "a<2 ; sys_platform == 'darwin'"] + +[packages.a.versions."1.0.0"] +requires = ["b"] +[packages.a.versions."2.0.0"] + +[packages.b.versions."1.0.0"] +requires = ["c"] + +[packages.c.versions."1.0.0"] +requires = ["d ; sys_platform == 'linux'"] + +[packages.d.versions."1.0.0"] diff --git a/resolver/testdata/packse/fork/marker-inherit.toml b/resolver/testdata/packse/fork/marker-inherit.toml new file mode 100644 index 0000000..03c3fed --- /dev/null +++ b/resolver/testdata/packse/fork/marker-inherit.toml @@ -0,0 +1,28 @@ +name = "fork-marker-inherit" +description = ''' +This tests that markers which provoked a fork in the universal resolver +are used to ignore dependencies which cannot possibly be installed by a +resolution produced by that fork. + +In this example, the `a<2` dependency is only active on Darwin +platforms. But the `a==1.0.0` distribution has a dependency on `b` +that is only active on Linux, where as `a==2.0.0` does not. Therefore, +when the fork provoked by the `a<2` dependency considers `b`, it should +ignore it because it isn't possible for `sys_platform == 'linux'` and +`sys_platform == 'darwin'` to be simultaneously true. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = ["a>=2 ; sys_platform == 'linux'", "a<2 ; sys_platform == 'darwin'"] + +[packages.a.versions."1.0.0"] +requires = ["b ; sys_platform == 'linux'"] +[packages.a.versions."2.0.0"] + +[packages.b.versions."1.0.0"] diff --git a/resolver/testdata/packse/fork/marker-limited-inherit.toml b/resolver/testdata/packse/fork/marker-limited-inherit.toml new file mode 100644 index 0000000..fa167aa --- /dev/null +++ b/resolver/testdata/packse/fork/marker-limited-inherit.toml @@ -0,0 +1,34 @@ +name = "fork-marker-limited-inherit" +description = ''' +This is like `fork-marker-inherit`, but it tests that dependency +filtering only occurs in the context of a fork. + +For example, as in `fork-marker-inherit`, the `c` dependency of +`a<2` should be entirely excluded here since it is possible for +`sys_platform` to be simultaneously equivalent to Darwin and Linux. +However, the unconditional dependency on `b`, which in turn depends on +`c` for Linux only, should still incorporate `c` as the dependency is +not part of any fork. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = [ + "a>=2 ; sys_platform == 'linux'", + "a<2 ; sys_platform == 'darwin'", + "b", +] + +[packages.a.versions."1.0.0"] +requires = ["c ; sys_platform == 'linux'"] +[packages.a.versions."2.0.0"] + +[packages.b.versions."1.0.0"] +requires = ["c ; sys_platform == 'linux'"] + +[packages.c.versions."1.0.0"] diff --git a/resolver/testdata/packse/fork/marker-selection.toml b/resolver/testdata/packse/fork/marker-selection.toml new file mode 100644 index 0000000..bc697e9 --- /dev/null +++ b/resolver/testdata/packse/fork/marker-selection.toml @@ -0,0 +1,28 @@ +name = "fork-marker-selection" +description = ''' +This tests a case where the resolver forks because of non-overlapping marker +expressions on `b`. In the original universal resolver implementation, this +resulted in multiple versions of `a` being unconditionally included in the lock +file. So this acts as a regression test to ensure that only one version of `a` +is selected. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = [ + "a", + "b>=2 ; sys_platform == 'linux'", + "b<2 ; sys_platform == 'darwin'", +] + +[packages.a.versions."0.1.0"] +[packages.a.versions."0.2.0"] +requires = ["b>=2.0.0"] + +[packages.b.versions."1.0.0"] +[packages.b.versions."2.0.0"] diff --git a/resolver/testdata/packse/fork/marker-track.toml b/resolver/testdata/packse/fork/marker-track.toml new file mode 100644 index 0000000..3f0a3d5 --- /dev/null +++ b/resolver/testdata/packse/fork/marker-track.toml @@ -0,0 +1,30 @@ +name = "fork-marker-track" +description = ''' +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = [ + "a", + "b>=2.8 ; sys_platform == 'linux'", + "b<2.8 ; sys_platform == 'darwin'", +] + +[packages.a.versions."1.3.1"] +requires = ["c ; implementation_name == 'iron'"] +[packages.a.versions."2.0.0"] +requires = ["b>=2.8", "c ; implementation_name == 'cpython'"] +[packages.a.versions."3.1.0"] +requires = ["b>=2.8", "c ; implementation_name == 'pypy'"] +[packages.a.versions."4.3.0"] +requires = ["b>=2.8"] + +[packages.b.versions."2.7"] +[packages.b.versions."2.8"] + +[packages.c.versions."1.10"] diff --git a/resolver/testdata/packse/fork/non-fork-marker-transitive.toml b/resolver/testdata/packse/fork/non-fork-marker-transitive.toml new file mode 100644 index 0000000..ecbe97a --- /dev/null +++ b/resolver/testdata/packse/fork/non-fork-marker-transitive.toml @@ -0,0 +1,24 @@ +name = "fork-non-fork-marker-transitive" +description = ''' +This is the same setup as `non-local-fork-marker-transitive`, but the disjoint +dependency specifications on `c` use the same constraints and thus depend on +the same version of `c`. In this case, there is no conflict. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = ["a==1.0.0", "b==1.0.0"] + +[packages.a.versions."1.0.0"] +requires = ["c>=2.0.0 ; sys_platform == 'linux'"] + +[packages.b.versions."1.0.0"] +requires = ["c>=2.0.0 ; sys_platform == 'darwin'"] + +[packages.c.versions."1.0.0"] +[packages.c.versions."2.0.0"] diff --git a/resolver/testdata/packse/fork/non-local-fork-marker-direct.toml b/resolver/testdata/packse/fork/non-local-fork-marker-direct.toml new file mode 100644 index 0000000..23c44b3 --- /dev/null +++ b/resolver/testdata/packse/fork/non-local-fork-marker-direct.toml @@ -0,0 +1,28 @@ +name = "fork-non-local-fork-marker-direct" +description = ''' +This is like `non-local-fork-marker-transitive`, but the marker expressions are +placed on sibling dependency specifications. However, the actual dependency on +`c` is indirect, and thus, there's no fork detected by the universal resolver. +This in turn results in an unresolvable conflict on `c`. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = false + +[root] +requires = [ + "a==1.0.0 ; sys_platform == 'linux'", + "b==1.0.0 ; sys_platform == 'darwin'", +] + +[packages.a.versions."1.0.0"] +requires = ["c<2.0.0"] + +[packages.b.versions."1.0.0"] +requires = ["c>=2.0.0"] + +[packages.c.versions."1.0.0"] +[packages.c.versions."2.0.0"] diff --git a/resolver/testdata/packse/fork/non-local-fork-marker-transitive.toml b/resolver/testdata/packse/fork/non-local-fork-marker-transitive.toml new file mode 100644 index 0000000..19b48e3 --- /dev/null +++ b/resolver/testdata/packse/fork/non-local-fork-marker-transitive.toml @@ -0,0 +1,31 @@ +name = "fork-non-local-fork-marker-transitive" +description = ''' +This setup introduces dependencies on two distinct versions of `c`, where +each such dependency has a marker expression attached that would normally +make them disjoint. In a non-universal resolver, this is no problem. But in a +forking resolver that tries to create one universal resolution, this can lead +to two distinct versions of `c` in the resolution. This is in and of itself +not a problem, since that is an expected scenario for universal resolution. +The problem in this case is that because the dependency specifications for +`c` occur in two different points (i.e., they are not sibling dependency +specifications) in the dependency graph, the forking resolver does not "detect" +it, and thus never forks and thus this results in "no resolution." +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = false + +[root] +requires = ["a==1.0.0", "b==1.0.0"] + +[packages.a.versions."1.0.0"] +requires = ["c<2.0.0 ; sys_platform == 'linux'"] + +[packages.b.versions."1.0.0"] +requires = ["c>=2.0.0 ; sys_platform == 'darwin'"] + +[packages.c.versions."1.0.0"] +[packages.c.versions."2.0.0"] diff --git a/resolver/testdata/packse/fork/overlapping-markers-basic.toml b/resolver/testdata/packse/fork/overlapping-markers-basic.toml new file mode 100644 index 0000000..410ff39 --- /dev/null +++ b/resolver/testdata/packse/fork/overlapping-markers-basic.toml @@ -0,0 +1,48 @@ +name = "fork-overlapping-markers-basic" +description = ''' +This scenario tests a very basic case of overlapping markers. Namely, +it emulates a common pattern in the ecosystem where marker expressions +are used to progressively increase the version constraints of a package +as the Python version increases. + +In this case, there is actually a split occurring between +`python_version < '3.13'` and the other marker expressions, so this +isn't just a scenario with overlapping but non-disjoint markers. + +In particular, this serves as a regression test. uv used to create a +lock file with a dependency on `a` with the following markers: + + python_version < '3.13' or python_version >= '3.14' + +But this implies that `a` won't be installed for Python 3.13, which is +clearly wrong. + +The issue was that uv was intersecting *all* marker expressions. So +that `a>=1.1.0` and `a>=1.2.0` fork was getting `python_version >= +'3.13' and python_version >= '3.14'`, which, of course, simplifies +to `python_version >= '3.14'`. But this is wrong! It should be +`python_version >= '3.13' or python_version >= '3.14'`, which of course +simplifies to `python_version >= '3.13'`. And thus, the resulting forks +are not just disjoint but complete in this case. + +Since there are no other constraints on `a`, this causes uv to select +`1.2.0` unconditionally. (The marker expressions get normalized out +entirely.) +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = [ + "a>=1.0.0 ; python_version < '3.13'", + "a>=1.1.0 ; python_version >= '3.13'", + "a>=1.2.0 ; python_version >= '3.14'", +] + +[packages.a.versions."1.0.0"] +[packages.a.versions."1.1.0"] +[packages.a.versions."1.2.0"] diff --git a/resolver/testdata/packse/fork/preferences-dependent-forking-bistable.toml b/resolver/testdata/packse/fork/preferences-dependent-forking-bistable.toml new file mode 100644 index 0000000..735c2c6 --- /dev/null +++ b/resolver/testdata/packse/fork/preferences-dependent-forking-bistable.toml @@ -0,0 +1,77 @@ +name = "preferences-dependent-forking-bistable" +description = ''' +This test contains a bistable resolution scenario when not using ahead-of-time +splitting of resolution forks: We meet one of two fork points depending on the +preferences, creating a resolution whose preferences lead us the other fork +point. + +In the first case, we are in cleaver 2 and fork on `sys_platform`, in the +second case, we are in foo 1 or bar 1 amd fork over `os_name`. + +First case: We select cleaver 2, fork on `sys_platform`, we reject cleaver 2 +(missing fork `os_name`), we select cleaver 1 and don't fork on `os_name` in +`fork-if-not-forked`, done. +Second case: We have preference cleaver 1, fork on `os_name` in +`fork-if-not-forked`, we reject cleaver 1, we select cleaver 2, we fork on +`sys_platform`, we accept cleaver 2 since we forked on `os_name`, done. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = ["cleaver"] + +[packages.cleaver.versions."2.0.0"] +requires = [ + # Provoke a fork on sys_platform. + "fork-sys-platform==1; sys_platform == 'linux'", + "fork-sys-platform==2; sys_platform != 'linux'", + # Reject cleaver 2 if we didn't fork on os_name, without forking on os_name. + "reject-cleaver2==1; os_name == 'posix'", + "reject-cleaver2-proxy", +] + +[packages.fork-sys-platform.versions."1.0.0"] +[packages.fork-sys-platform.versions."2.0.0"] + +[packages.reject-cleaver2-proxy.versions."1.0.0"] +requires = ["reject-cleaver2==2; os_name != 'posix'"] + +[packages.reject-cleaver2.versions."1.0.0"] +[packages.reject-cleaver2.versions."2.0.0"] + +[packages.cleaver.versions."1.0.0"] +requires = [ + # Provoke a fork on os-name, but only if we didn't fork before. + "fork-if-not-forked!=2; sys_platform == 'linux'", + "fork-if-not-forked-proxy; sys_platform != 'linux'", + # Reject cleaver 1 if we didn't fork on sys_platform, without forking on sys_platform. + "reject-cleaver1==1; sys_platform == 'linux'", + "reject-cleaver1-proxy", +] + +[packages.fork-if-not-forked-proxy.versions."1.0.0"] +requires = ["fork-if-not-forked!=3"] + +[packages.reject-cleaver1-proxy.versions."1.0.0"] +requires = ["reject-cleaver1==2; sys_platform != 'linux'"] + +[packages.reject-cleaver1.versions."1.0.0"] +[packages.reject-cleaver1.versions."2.0.0"] + +[packages.fork-os-name.versions."1.0.0"] +[packages.fork-os-name.versions."2.0.0"] + +[packages.fork-if-not-forked.versions."1.0.0"] +requires = [ + # Actually provoke the fork for cleaver 1. + "fork-os-name==1; os_name == 'posix'", + "fork-os-name==2; os_name != 'posix'", + "reject-cleaver1-proxy", +] +[packages.fork-if-not-forked.versions."2.0.0"] +[packages.fork-if-not-forked.versions."3.0.0"] diff --git a/resolver/testdata/packse/fork/preferences-dependent-forking-conflicting.toml b/resolver/testdata/packse/fork/preferences-dependent-forking-conflicting.toml new file mode 100644 index 0000000..814eec7 --- /dev/null +++ b/resolver/testdata/packse/fork/preferences-dependent-forking-conflicting.toml @@ -0,0 +1,79 @@ +name = "preferences-dependent-forking-conflicting" +description = ''' +Like `preferences-dependent-forking`, but when we don't fork the resolution fails. + +Consider a fresh run without preferences: +* We start with cleaver 2 +* We fork +* We reject cleaver 2 +* We find cleaver solution in fork 1 with foo 2 with bar 1 +* We find cleaver solution in fork 2 with foo 1 with bar 2 +* We write cleaver 1, foo 1, foo 2, bar 1 and bar 2 to the lockfile + +In a subsequent run, we read the preference cleaver 1 from the lockfile (the preferences for foo and bar don't matter): +* We start with cleaver 1 +* We're in universal mode, cleaver requires foo 1, bar 1 +* foo 1 requires bar 2, conflict + +Design sketch: +```text +root -> clear, foo, bar +# Cause a fork, then forget that version. +cleaver 2 -> unrelated-dep==1; fork==1 +cleaver 2 -> unrelated-dep==2; fork==2 +cleaver 2 -> reject-cleaver-2 +# Allow different versions when forking, but force foo 1, bar 1 in universal mode without forking. +cleaver 1 -> foo==1; fork==1 +cleaver 1 -> bar==1; fork==2 +# When we selected foo 1, bar 1 in universal mode for cleaver, this causes a conflict, otherwise we select bar 2. +foo 1 -> bar==2 +``` +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = false + +[root] +requires = [ + "cleaver", + # The reporter packages. + "foo", + "bar", +] + +[packages.cleaver.versions."2.0.0"] +requires = [ + # Provoke a fork + "unrelated-dep==1; sys_platform == 'linux'", + "unrelated-dep==2; sys_platform != 'linux'", + "reject-cleaver-2", +] + +[packages.reject-cleaver-2.versions."1.0.0"] +requires = [ + # That's a conflict with `cleaver==2`. + "unrelated-dep==3", +] + +[packages.unrelated-dep.versions."1.0.0"] +[packages.unrelated-dep.versions."2.0.0"] +[packages.unrelated-dep.versions."3.0.0"] + +[packages.cleaver.versions."1.0.0"] +requires = [ + # Allow different versions when forking, but force foo 1, bar 1 in universal mode without forking. + "foo==1; sys_platform == 'linux'", + "bar==1; sys_platform != 'linux'", +] + +[packages.foo.versions."1.0.0"] +requires = [ + # When we selected foo 1, bar 1 in universal mode for cleaver, this causes a conflict, otherwise we select bar 2. + "bar==2", +] +[packages.foo.versions."2.0.0"] +[packages.bar.versions."1.0.0"] +[packages.bar.versions."2.0.0"] diff --git a/resolver/testdata/packse/fork/preferences-dependent-forking-tristable.toml b/resolver/testdata/packse/fork/preferences-dependent-forking-tristable.toml new file mode 100644 index 0000000..b2162e0 --- /dev/null +++ b/resolver/testdata/packse/fork/preferences-dependent-forking-tristable.toml @@ -0,0 +1,86 @@ +name = "preferences-dependent-forking-tristable" +description = ''' +This test case is like "preferences-dependent-forking-bistable", but with three +states instead of two. The first two locks are in a different state, then we +enter the tristable state. + +It's not polished, but it's useful to have something with a higher period +than 2 in our test suite. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = ["cleaver", "foo", "bar"] + +[packages.cleaver.versions."2.0.0"] +requires = [ + "unrelated-dep==1; sys_platform == 'linux'", + "unrelated-dep==2; sys_platform != 'linux'", + "a", + "b", +] + +[packages.a.versions."1.0.0"] +requires = ["unrelated-dep3==1; os_name == 'posix'"] + +[packages.b.versions."1.0.0"] +requires = ["unrelated-dep3==2; os_name != 'posix'"] + +[packages.reject-cleaver-2.versions."1.0.0"] +requires = ["unrelated-dep3==3"] + +[packages.unrelated-dep.versions."1.0.0"] +[packages.unrelated-dep.versions."2.0.0"] +[packages.unrelated-dep.versions."3.0.0"] + +[packages.unrelated-dep3.versions."1.0.0"] +[packages.unrelated-dep3.versions."2.0.0"] +[packages.unrelated-dep3.versions."3.0.0"] + +[packages.cleaver.versions."1.0.0"] +requires = [ + "foo==1; sys_platform == 'linux'", + "bar==1; sys_platform != 'linux'", +] + +[packages.foo.versions."1.0.0"] +requires = [ + "c!=3; sys_platform == 'linux'", + "c!=2; sys_platform != 'linux'", + "reject-cleaver-1", +] +[packages.foo.versions."2.0.0"] +[packages.bar.versions."1.0.0"] +requires = [ + "c!=3; sys_platform == 'linux'", + "d; sys_platform != 'linux'", + "reject-cleaver-1", +] +[packages.bar.versions."2.0.0"] + +[packages.reject-cleaver-1.versions."1.0.0"] +requires = [ + "unrelated-dep2==1; sys_platform == 'linux'", + "unrelated-dep2==2; sys_platform != 'linux'", +] + +[packages.unrelated-dep2.versions."1.0.0"] +[packages.unrelated-dep2.versions."2.0.0"] +[packages.unrelated-dep2.versions."3.0.0"] + +[packages.c.versions."1.0.0"] +requires = [ + "unrelated-dep2==1; os_name == 'posix'", + "unrelated-dep2==2; os_name != 'posix'", + "reject-cleaver-1", +] +[packages.c.versions."2.0.0"] +[packages.c.versions."3.0.0"] + +[packages.d.versions."1.0.0"] +requires = ["c!=2"] diff --git a/resolver/testdata/packse/fork/preferences-dependent-forking.toml b/resolver/testdata/packse/fork/preferences-dependent-forking.toml new file mode 100644 index 0000000..ccea44c --- /dev/null +++ b/resolver/testdata/packse/fork/preferences-dependent-forking.toml @@ -0,0 +1,76 @@ +name = "preferences-dependent-forking" +description = ''' +This test contains a scenario where the solution depends on whether we fork, and whether we fork depends on the +preferences. + +Consider a fresh run without preferences: +* We start with cleaver 2 +* We fork +* We reject cleaver 2 +* We find cleaver solution in fork 1 with foo 2 with bar 1 +* We find cleaver solution in fork 2 with foo 1 with bar 2 +* We write cleaver 1, foo 1, foo 2, bar 1 and bar 2 to the lockfile + +In a subsequent run, we read the preference cleaver 1 from the lockfile (the preferences for foo and bar don't matter): +* We start with cleaver 1 +* We're in universal mode, we resolve foo 1 and bar 1 +* We write cleaver 1 and bar 1 to the lockfile + +We call a resolution that's different on the second run to the first unstable. + +Design sketch: +```text +root -> clear, foo, bar +# Cause a fork, then forget that version. +cleaver 2 -> unrelated-dep==1; fork==1 +cleaver 2 -> unrelated-dep==2; fork==2 +cleaver 2 -> reject-cleaver-2 +# Allow different versions when forking, but force foo 1, bar 1 in universal mode without forking. +cleaver 1 -> foo==1; fork==1 +cleaver 1 -> bar==1; fork==2 +``` +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = [ + "cleaver", + # The reporter packages. + "foo", + "bar", +] + +[packages.cleaver.versions."2.0.0"] +requires = [ + # Provoke a fork + "unrelated-dep==1; sys_platform == 'linux'", + "unrelated-dep==2; sys_platform != 'linux'", + "reject-cleaver-2", +] + +[packages.reject-cleaver-2.versions."1.0.0"] +requires = [ + # That's a conflict with `cleaver==2`. + "unrelated-dep==3", +] + +[packages.unrelated-dep.versions."1.0.0"] +[packages.unrelated-dep.versions."2.0.0"] +[packages.unrelated-dep.versions."3.0.0"] + +[packages.cleaver.versions."1.0.0"] +requires = [ + # Allow different versions when forking, but force foo 1, bar 1 in universal mode without forking. + "foo==1; sys_platform == 'linux'", + "bar==1; sys_platform != 'linux'", +] + +[packages.foo.versions."1.0.0"] +[packages.foo.versions."2.0.0"] +[packages.bar.versions."1.0.0"] +[packages.bar.versions."2.0.0"] diff --git a/resolver/testdata/packse/fork/remaining-universe-partitioning.toml b/resolver/testdata/packse/fork/remaining-universe-partitioning.toml new file mode 100644 index 0000000..dcfe62d --- /dev/null +++ b/resolver/testdata/packse/fork/remaining-universe-partitioning.toml @@ -0,0 +1,47 @@ +name = "fork-remaining-universe-partitioning" +description = ''' +This scenario tries to check that the "remaining universe" handling in +the universal resolver is correct. Namely, whenever we create forks +from disjoint markers that don't union to the universe, we need to +create *another* fork corresponding to the difference between the +universe and the union of the forks. + +But when we do this, that remaining universe fork needs to be created +like any other fork: it should start copying whatever set of forks +existed by the time we got to this point, intersecting the markers with +the markers describing the remaining universe and then filtering out +any dependencies that are disjoint with the resulting markers. + +This test exercises that logic by ensuring that a package `z` in the +remaining universe is excluded based on the combination of markers +from a parent fork. That is, if the remaining universe fork does not +pick up the markers from the parent forks, then `z` would be included +because the remaining universe for _just_ the `b` dependencies of `a` +is `os_name != 'linux' and os_name != 'darwin'`, which is satisfied by +`z`'s marker of `sys_platform == 'windows'`. However, `a 1.0.0` is only +selected in the context of `a < 2 ; sys_platform == 'illumos'`, so `z` +should never appear in the resolution. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[root] +requires = [ + "a>=2 ; sys_platform == 'windows'", + "a<2 ; sys_platform == 'illumos'", +] + +[packages.z.versions."1.0.0"] +[packages.a.versions."1.0.0"] +requires = [ + "b>=2 ; os_name == 'linux'", + "b<2 ; os_name == 'darwin'", + "z ; sys_platform == 'windows'", +] +[packages.a.versions."2.0.0"] +[packages.b.versions."1.0.0"] +[packages.b.versions."2.0.0"] diff --git a/resolver/testdata/packse/fork/requires-python-full-prerelease.toml b/resolver/testdata/packse/fork/requires-python-full-prerelease.toml new file mode 100644 index 0000000..8e608a9 --- /dev/null +++ b/resolver/testdata/packse/fork/requires-python-full-prerelease.toml @@ -0,0 +1,23 @@ +name = "fork-requires-python-full-prerelease" +description = ''' +This tests that a `Requires-Python` specifier will result in the +exclusion of dependency specifications that cannot possibly satisfy it. + +In particular, this is tested via the `python_full_version` marker with +a pre-release version. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[environment] +python = "3.12" + +[root] +requires_python = ">=3.10" +requires = ["a==1.0.0 ; python_full_version == '3.9b1'"] + +[packages.a.versions."1.0.0"] diff --git a/resolver/testdata/packse/fork/requires-python-full.toml b/resolver/testdata/packse/fork/requires-python-full.toml new file mode 100644 index 0000000..b2e72d8 --- /dev/null +++ b/resolver/testdata/packse/fork/requires-python-full.toml @@ -0,0 +1,23 @@ +name = "fork-requires-python-full" +description = ''' +This tests that a `Requires-Python` specifier will result in the +exclusion of dependency specifications that cannot possibly satisfy it. + +In particular, this is tested via the `python_full_version` marker +instead of the more common `python_version` marker. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[environment] +python = "3.12" + +[root] +requires_python = ">=3.10" +requires = ["a==1.0.0 ; python_full_version == '3.9'"] + +[packages.a.versions."1.0.0"] diff --git a/resolver/testdata/packse/fork/requires-python-patch-overlap.toml b/resolver/testdata/packse/fork/requires-python-patch-overlap.toml new file mode 100644 index 0000000..332a5a4 --- /dev/null +++ b/resolver/testdata/packse/fork/requires-python-patch-overlap.toml @@ -0,0 +1,28 @@ +name = "fork-requires-python-patch-overlap" +description = ''' +This tests that a `Requires-Python` specifier that includes a Python +patch version will not result in excluded a dependency specification +with a `python_version == '3.10'` marker. + +This is a regression test for the universal resolver where it would +convert a `Requires-Python: >=3.10.1` specifier into a +`python_version >= '3.10.1'` marker expression, which would be +considered disjoint with `python_version == '3.10'`. Thus, the +dependency `a` below was erroneously excluded. It should be included. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[environment] +python = "3.12" + +[root] +requires_python = ">=3.10.1" +requires = ["a==1.0.0 ; python_version == '3.10'"] + +[packages.a.versions."1.0.0"] +requires_python = ">=3.10" diff --git a/resolver/testdata/packse/fork/requires-python.toml b/resolver/testdata/packse/fork/requires-python.toml new file mode 100644 index 0000000..47295a7 --- /dev/null +++ b/resolver/testdata/packse/fork/requires-python.toml @@ -0,0 +1,20 @@ +name = "fork-requires-python" +description = ''' +This tests that a `Requires-Python` specifier will result in the +exclusion of dependency specifications that cannot possibly satisfy it. +''' + +[resolver_options] +universal = true + +[expected] +satisfiable = true + +[environment] +python = "3.12" + +[root] +requires_python = ">=3.10" +requires = ["a==1.0.0 ; python_version == '3.9'"] + +[packages.a.versions."1.0.0"] diff --git a/resolver/testdata/packse/incompatible_versions/direct-incompatible-versions.toml b/resolver/testdata/packse/incompatible_versions/direct-incompatible-versions.toml new file mode 100644 index 0000000..c05bc72 --- /dev/null +++ b/resolver/testdata/packse/incompatible_versions/direct-incompatible-versions.toml @@ -0,0 +1,12 @@ +name = "direct-incompatible-versions" +description = "The user requires two incompatible, existing versions of package `a`" + +[root] +requires = ["a==1.0.0", "a==2.0.0"] + +[expected] +satisfiable = false + +[packages.a.versions."1.0.0"] + +[packages.a.versions."2.0.0"] diff --git a/resolver/testdata/packse/incompatible_versions/transitive-incompatible-versions.toml b/resolver/testdata/packse/incompatible_versions/transitive-incompatible-versions.toml new file mode 100644 index 0000000..4650a46 --- /dev/null +++ b/resolver/testdata/packse/incompatible_versions/transitive-incompatible-versions.toml @@ -0,0 +1,11 @@ +name = "transitive-incompatible-versions" +description = "The user requires `a`, which requires two incompatible, existing versions of package `b`" + +[root] +requires = ["a==1.0.0"] + +[expected] +satisfiable = false + +[packages.a.versions."1.0.0"] +requires = ["b==2.0.0", "b==1.0.0"] diff --git a/resolver/testdata/packse/incompatible_versions/transitive-incompatible-with-root-version.toml b/resolver/testdata/packse/incompatible_versions/transitive-incompatible-with-root-version.toml new file mode 100644 index 0000000..3dd1c14 --- /dev/null +++ b/resolver/testdata/packse/incompatible_versions/transitive-incompatible-with-root-version.toml @@ -0,0 +1,15 @@ +name = "transitive-incompatible-with-root-version" +description = "The user requires packages `a` and `b` but `a` requires a different version of `b`" + +[root] +requires = ["a", "b==1.0.0"] + +[expected] +satisfiable = false + +[packages.a.versions."1.0.0"] +requires = ["b==2.0.0"] + +[packages.b.versions."1.0.0"] + +[packages.b.versions."2.0.0"] diff --git a/resolver/testdata/packse/incompatible_versions/transitive-incompatible-with-transitive.toml b/resolver/testdata/packse/incompatible_versions/transitive-incompatible-with-transitive.toml new file mode 100644 index 0000000..e4e12f8 --- /dev/null +++ b/resolver/testdata/packse/incompatible_versions/transitive-incompatible-with-transitive.toml @@ -0,0 +1,18 @@ +name = "transitive-incompatible-with-transitive" +description = "The user requires package `a` and `b`; `a` and `b` require different versions of `c`" + +[root] +requires = ["a", "b"] + +[expected] +satisfiable = false + +[packages.a.versions."1.0.0"] +requires = ["c==1.0.0"] + +[packages.b.versions."1.0.0"] +requires = ["c==2.0.0"] + +[packages.c.versions."1.0.0"] + +[packages.c.versions."2.0.0"] diff --git a/resolver/testdata/packse/local/local-greater-than-or-equal.toml b/resolver/testdata/packse/local/local-greater-than-or-equal.toml new file mode 100644 index 0000000..0342a0c --- /dev/null +++ b/resolver/testdata/packse/local/local-greater-than-or-equal.toml @@ -0,0 +1,16 @@ +name = "local-greater-than-or-equal" +description = "A local version should be included in inclusive ordered comparisons." + +[root] +requires = ["a>=1.2.3"] + +[expected] +satisfiable = true +explanation = "The version '1.2.3+foo' satisfies the constraint '>=1.2.3'." + +[expected.packages] +a = "1.2.3+foo" + +[packages.a.versions."1.2.3+bar"] + +[packages.a.versions."1.2.3+foo"] diff --git a/resolver/testdata/packse/local/local-greater-than.toml b/resolver/testdata/packse/local/local-greater-than.toml new file mode 100644 index 0000000..2ffff88 --- /dev/null +++ b/resolver/testdata/packse/local/local-greater-than.toml @@ -0,0 +1,10 @@ +name = "local-greater-than" +description = "A local version should be excluded in exclusive ordered comparisons." + +[root] +requires = ["a>1.2.3"] + +[expected] +satisfiable = false + +[packages.a.versions."1.2.3+foo"] diff --git a/resolver/testdata/packse/local/local-less-than-or-equal.toml b/resolver/testdata/packse/local/local-less-than-or-equal.toml new file mode 100644 index 0000000..0e1c24c --- /dev/null +++ b/resolver/testdata/packse/local/local-less-than-or-equal.toml @@ -0,0 +1,16 @@ +name = "local-less-than-or-equal" +description = "A local version should be included in inclusive ordered comparisons." + +[root] +requires = ["a<=1.2.3"] + +[expected] +satisfiable = true +explanation = "The version '1.2.3+foo' satisfies the constraint '<=1.2.3'." + +[expected.packages] +a = "1.2.3+foo" + +[packages.a.versions."1.2.3+bar"] + +[packages.a.versions."1.2.3+foo"] diff --git a/resolver/testdata/packse/local/local-less-than.toml b/resolver/testdata/packse/local/local-less-than.toml new file mode 100644 index 0000000..0e63ea4 --- /dev/null +++ b/resolver/testdata/packse/local/local-less-than.toml @@ -0,0 +1,10 @@ +name = "local-less-than" +description = "A local version should be excluded in exclusive ordered comparisons." + +[root] +requires = ["a<1.2.3"] + +[expected] +satisfiable = false + +[packages.a.versions."1.2.3+foo"] diff --git a/resolver/testdata/packse/local/local-not-latest.toml b/resolver/testdata/packse/local/local-not-latest.toml new file mode 100644 index 0000000..324848a --- /dev/null +++ b/resolver/testdata/packse/local/local-not-latest.toml @@ -0,0 +1,22 @@ +name = "local-not-latest" +description = "Tests that we can select an older version with a local segment when newer versions are incompatible." + +[root] +requires = ["a>=1"] + +[expected] +satisfiable = true + +[expected.packages] +a = "1.2.1+foo" + +[packages.a.versions."1.2.3"] +wheel_tags = ["py3-any-macosx_10_0_ppc64"] +sdist = false + +[packages.a.versions."1.2.2+foo"] +wheel_tags = ["py3-any-macosx_10_0_ppc64"] +sdist = false + +[packages.a.versions."1.2.1+foo"] +sdist = true diff --git a/resolver/testdata/packse/local/local-not-used-with-sdist.toml b/resolver/testdata/packse/local/local-not-used-with-sdist.toml new file mode 100644 index 0000000..b1de6a1 --- /dev/null +++ b/resolver/testdata/packse/local/local-not-used-with-sdist.toml @@ -0,0 +1,18 @@ +name = "local-not-used-with-sdist" +description = "If there is a 1.2.3 version with an sdist published and no compatible wheels, then the sdist will be used." + +[root] +requires = ["a==1.2.3"] + +[expected] +satisfiable = true +explanation = "The version '1.2.3' with an sdist satisfies the constraint '==1.2.3'." + +[expected.packages] +a = "1.2.3+foo" + +[packages.a.versions."1.2.3"] +wheel_tags = ["py3-any-macosx_10_0_ppc64"] +sdist = true + +[packages.a.versions."1.2.3+foo"] diff --git a/resolver/testdata/packse/local/local-simple.toml b/resolver/testdata/packse/local/local-simple.toml new file mode 100644 index 0000000..a9847c1 --- /dev/null +++ b/resolver/testdata/packse/local/local-simple.toml @@ -0,0 +1,16 @@ +name = "local-simple" +description = "A simple version constraint should not exclude published versions with local segments." + +[root] +requires = ["a==1.2.3"] + +[expected] +satisfiable = true +explanation = "The version '1.2.3+foo' satisfies the constraint '==1.2.3'." + +[expected.packages] +a = "1.2.3+foo" + +[packages.a.versions."1.2.3+bar"] + +[packages.a.versions."1.2.3+foo"] diff --git a/resolver/testdata/packse/local/local-transitive-backtrack.toml b/resolver/testdata/packse/local/local-transitive-backtrack.toml new file mode 100644 index 0000000..792c25b --- /dev/null +++ b/resolver/testdata/packse/local/local-transitive-backtrack.toml @@ -0,0 +1,23 @@ +name = "local-transitive-backtrack" +description = "A dependency depends on a conflicting local version of a direct dependency, but we can backtrack to a compatible version." + +[root] +requires = ["a", "b==2.0.0+foo"] + +[expected] +satisfiable = true +explanation = "Backtracking to '1.0.0' gives us compatible local versions of b." + +[expected.packages] +a = "1.0.0" +b = "2.0.0+foo" + +[packages.a.versions."1.0.0"] +requires = ["b==2.0.0"] + +[packages.a.versions."2.0.0"] +requires = ["b==2.0.0+bar"] + +[packages.b.versions."2.0.0+bar"] + +[packages.b.versions."2.0.0+foo"] diff --git a/resolver/testdata/packse/local/local-transitive-conflicting.toml b/resolver/testdata/packse/local/local-transitive-conflicting.toml new file mode 100644 index 0000000..c3e4395 --- /dev/null +++ b/resolver/testdata/packse/local/local-transitive-conflicting.toml @@ -0,0 +1,15 @@ +name = "local-transitive-conflicting" +description = "A dependency depends on a conflicting local version of a direct dependency." + +[root] +requires = ["a", "b==2.0.0+foo"] + +[expected] +satisfiable = false + +[packages.a.versions."1.0.0"] +requires = ["b==2.0.0+bar"] + +[packages.b.versions."2.0.0+bar"] + +[packages.b.versions."2.0.0+foo"] diff --git a/resolver/testdata/packse/local/local-transitive-confounding.toml b/resolver/testdata/packse/local/local-transitive-confounding.toml new file mode 100644 index 0000000..0637e4b --- /dev/null +++ b/resolver/testdata/packse/local/local-transitive-confounding.toml @@ -0,0 +1,24 @@ +name = "local-transitive-confounding" +description = "A transitive dependency has both a non-local and local version published, but the non-local version is unusable." + +[root] +requires = ["a"] + +[expected] +satisfiable = true +explanation = "The version '2.0.0+foo' satisfies the constraint '==2.0.0'." + +[expected.packages] +a = "1.0.0" +b = "2.0.0+foo" + +[packages.a.versions."1.0.0"] +requires = ["b==2.0.0"] + +[packages.b.versions."2.0.0"] +wheel_tags = ["py3-any-macosx_10_0_ppc64"] +sdist = false + +[packages.b.versions."2.0.0+bar"] + +[packages.b.versions."2.0.0+foo"] diff --git a/resolver/testdata/packse/local/local-transitive-greater-than-or-equal.toml b/resolver/testdata/packse/local/local-transitive-greater-than-or-equal.toml new file mode 100644 index 0000000..08b8a97 --- /dev/null +++ b/resolver/testdata/packse/local/local-transitive-greater-than-or-equal.toml @@ -0,0 +1,20 @@ +name = "local-transitive-greater-than-or-equal" +description = "A transitive constraint on a local version should match an inclusive ordered operator." + +[root] +requires = ["a", "b==2.0.0+foo"] + +[expected] +satisfiable = true +explanation = "The version '2.0.0+foo' satisfies both >=2.0.0 and ==2.0.0+foo." + +[expected.packages] +a = "1.0.0" +b = "2.0.0+foo" + +[packages.a.versions."1.0.0"] +requires = ["b>=2.0.0"] + +[packages.b.versions."2.0.0+bar"] + +[packages.b.versions."2.0.0+foo"] diff --git a/resolver/testdata/packse/local/local-transitive-greater-than.toml b/resolver/testdata/packse/local/local-transitive-greater-than.toml new file mode 100644 index 0000000..d89eaa4 --- /dev/null +++ b/resolver/testdata/packse/local/local-transitive-greater-than.toml @@ -0,0 +1,15 @@ +name = "local-transitive-greater-than" +description = "A transitive constraint on a local version should not match an exclusive ordered operator." + +[root] +requires = ["a", "b==2.0.0+foo"] + +[expected] +satisfiable = false + +[packages.a.versions."1.0.0"] +requires = ["b>2.0.0"] + +[packages.b.versions."2.0.0+bar"] + +[packages.b.versions."2.0.0+foo"] diff --git a/resolver/testdata/packse/local/local-transitive-less-than-or-equal.toml b/resolver/testdata/packse/local/local-transitive-less-than-or-equal.toml new file mode 100644 index 0000000..f3fcd5f --- /dev/null +++ b/resolver/testdata/packse/local/local-transitive-less-than-or-equal.toml @@ -0,0 +1,20 @@ +name = "local-transitive-less-than-or-equal" +description = "A transitive constraint on a local version should match an inclusive ordered operator." + +[root] +requires = ["a", "b==2.0.0+foo"] + +[expected] +satisfiable = true +explanation = "The version '2.0.0+foo' satisfies both <=2.0.0 and ==2.0.0+foo." + +[expected.packages] +a = "1.0.0" +b = "2.0.0+foo" + +[packages.a.versions."1.0.0"] +requires = ["b<=2.0.0"] + +[packages.b.versions."2.0.0+bar"] + +[packages.b.versions."2.0.0+foo"] diff --git a/resolver/testdata/packse/local/local-transitive-less-than.toml b/resolver/testdata/packse/local/local-transitive-less-than.toml new file mode 100644 index 0000000..0c646b3 --- /dev/null +++ b/resolver/testdata/packse/local/local-transitive-less-than.toml @@ -0,0 +1,15 @@ +name = "local-transitive-less-than" +description = "A transitive constraint on a local version should not match an exclusive ordered operator." + +[root] +requires = ["a", "b==2.0.0+foo"] + +[expected] +satisfiable = false + +[packages.a.versions."1.0.0"] +requires = ["b<2.0.0"] + +[packages.b.versions."2.0.0+bar"] + +[packages.b.versions."2.0.0+foo"] diff --git a/resolver/testdata/packse/local/local-transitive.toml b/resolver/testdata/packse/local/local-transitive.toml new file mode 100644 index 0000000..fdb6a23 --- /dev/null +++ b/resolver/testdata/packse/local/local-transitive.toml @@ -0,0 +1,20 @@ +name = "local-transitive" +description = "A simple version constraint should not exclude published versions with local segments." + +[root] +requires = ["a", "b==2.0.0+foo"] + +[expected] +satisfiable = true +explanation = "The version '2.0.0+foo' satisfies both ==2.0.0 and ==2.0.0+foo." + +[expected.packages] +a = "1.0.0" +b = "2.0.0+foo" + +[packages.a.versions."1.0.0"] +requires = ["b==2.0.0"] + +[packages.b.versions."2.0.0+foo"] + +[packages.b.versions."2.0.0+bar"] diff --git a/resolver/testdata/packse/local/local-used-without-sdist.toml b/resolver/testdata/packse/local/local-used-without-sdist.toml new file mode 100644 index 0000000..2c8a7f0 --- /dev/null +++ b/resolver/testdata/packse/local/local-used-without-sdist.toml @@ -0,0 +1,18 @@ +name = "local-used-without-sdist" +description = "Even if there is a 1.2.3 version published, if it is unavailable for some reason (no sdist and no compatible wheels in this case), a 1.2.3 version with a local segment should be usable instead." + +[root] +requires = ["a==1.2.3"] + +[expected] +satisfiable = true +explanation = "The version '1.2.3+foo' satisfies the constraint '==1.2.3'." + +[expected.packages] +a = "1.2.3+foo" + +[packages.a.versions."1.2.3"] +wheel_tags = ["py3-any-macosx_10_0_ppc64"] +sdist = false + +[packages.a.versions."1.2.3+foo"] diff --git a/resolver/testdata/packse/post/post-equal-available.toml b/resolver/testdata/packse/post/post-equal-available.toml new file mode 100644 index 0000000..de4957b --- /dev/null +++ b/resolver/testdata/packse/post/post-equal-available.toml @@ -0,0 +1,16 @@ +name = "post-equal-available" +description = "An equal version constraint should match a post-release version if the post-release version is available." + +[root] +requires = ["a==1.2.3.post0"] + +[expected] +satisfiable = true +explanation = "The version '1.2.3.post0' satisfies the constraint '==1.2.3.post0'." + +[expected.packages] +a = "1.2.3.post0" + +[packages.a.versions."1.2.3"] + +[packages.a.versions."1.2.3.post0"] diff --git a/resolver/testdata/packse/post/post-equal-not-available.toml b/resolver/testdata/packse/post/post-equal-not-available.toml new file mode 100644 index 0000000..e214eae --- /dev/null +++ b/resolver/testdata/packse/post/post-equal-not-available.toml @@ -0,0 +1,12 @@ +name = "post-equal-not-available" +description = "An equal version constraint should not match a post-release version if the post-release version is not available." + +[root] +requires = ["a==1.2.3.post0"] + +[expected] +satisfiable = false + +[packages.a.versions."1.2.3"] + +[packages.a.versions."1.2.3.post1"] diff --git a/resolver/testdata/packse/post/post-greater-than-or-equal-post.toml b/resolver/testdata/packse/post/post-greater-than-or-equal-post.toml new file mode 100644 index 0000000..375b14e --- /dev/null +++ b/resolver/testdata/packse/post/post-greater-than-or-equal-post.toml @@ -0,0 +1,16 @@ +name = "post-greater-than-or-equal-post" +description = "A greater-than-or-equal version constraint should match a post-release version if the constraint is itself a post-release version." + +[root] +requires = ["a>=1.2.3.post0"] + +[expected] +satisfiable = true +explanation = "The version '1.2.3.post1' satisfies the constraint '>=1.2.3.post0'." + +[expected.packages] +a = "1.2.3.post1" + +[packages.a.versions."1.2.3.post0"] + +[packages.a.versions."1.2.3.post1"] diff --git a/resolver/testdata/packse/post/post-greater-than-or-equal.toml b/resolver/testdata/packse/post/post-greater-than-or-equal.toml new file mode 100644 index 0000000..66b5e26 --- /dev/null +++ b/resolver/testdata/packse/post/post-greater-than-or-equal.toml @@ -0,0 +1,14 @@ +name = "post-greater-than-or-equal" +description = "A greater-than-or-equal version constraint should match a post-release version." + +[root] +requires = ["a>=1.2.3"] + +[expected] +satisfiable = true +explanation = "The version '1.2.3.post1' satisfies the constraint '>=1.2.3'." + +[expected.packages] +a = "1.2.3.post1" + +[packages.a.versions."1.2.3.post1"] diff --git a/resolver/testdata/packse/post/post-greater-than-post-not-available.toml b/resolver/testdata/packse/post/post-greater-than-post-not-available.toml new file mode 100644 index 0000000..075c869 --- /dev/null +++ b/resolver/testdata/packse/post/post-greater-than-post-not-available.toml @@ -0,0 +1,14 @@ +name = "post-greater-than-post-not-available" +description = "A greater-than version constraint should not match a post-release version if the post-release version is not available." + +[root] +requires = ["a>1.2.3.post2"] + +[expected] +satisfiable = false + +[packages.a.versions."1.2.3"] + +[packages.a.versions."1.2.3.post0"] + +[packages.a.versions."1.2.3.post1"] diff --git a/resolver/testdata/packse/post/post-greater-than-post.toml b/resolver/testdata/packse/post/post-greater-than-post.toml new file mode 100644 index 0000000..0478f08 --- /dev/null +++ b/resolver/testdata/packse/post/post-greater-than-post.toml @@ -0,0 +1,16 @@ +name = "post-greater-than-post" +description = "A greater-than version constraint should match a post-release version if the constraint is itself a post-release version." + +[root] +requires = ["a>1.2.3.post0"] + +[expected] +satisfiable = true +explanation = "The version '1.2.3.post1' satisfies the constraint '>1.2.3.post0'." + +[expected.packages] +a = "1.2.3.post1" + +[packages.a.versions."1.2.3.post0"] + +[packages.a.versions."1.2.3.post1"] diff --git a/resolver/testdata/packse/post/post-greater-than.toml b/resolver/testdata/packse/post/post-greater-than.toml new file mode 100644 index 0000000..c62e2a9 --- /dev/null +++ b/resolver/testdata/packse/post/post-greater-than.toml @@ -0,0 +1,10 @@ +name = "post-greater-than" +description = "A greater-than version constraint should not match a post-release version." + +[root] +requires = ["a>1.2.3"] + +[expected] +satisfiable = false + +[packages.a.versions."1.2.3.post1"] diff --git a/resolver/testdata/packse/post/post-less-than-or-equal.toml b/resolver/testdata/packse/post/post-less-than-or-equal.toml new file mode 100644 index 0000000..a5d3189 --- /dev/null +++ b/resolver/testdata/packse/post/post-less-than-or-equal.toml @@ -0,0 +1,10 @@ +name = "post-less-than-or-equal" +description = "A less-than-or-equal version constraint should not match a post-release version." + +[root] +requires = ["a<=1.2.3"] + +[expected] +satisfiable = false + +[packages.a.versions."1.2.3.post1"] diff --git a/resolver/testdata/packse/post/post-less-than.toml b/resolver/testdata/packse/post/post-less-than.toml new file mode 100644 index 0000000..826d3a7 --- /dev/null +++ b/resolver/testdata/packse/post/post-less-than.toml @@ -0,0 +1,10 @@ +name = "post-less-than" +description = "A less-than version constraint should not match a post-release version." + +[root] +requires = ["a<1.2.3"] + +[expected] +satisfiable = false + +[packages.a.versions."1.2.3.post1"] diff --git a/resolver/testdata/packse/post/post-local-greater-than-post.toml b/resolver/testdata/packse/post/post-local-greater-than-post.toml new file mode 100644 index 0000000..b03335b --- /dev/null +++ b/resolver/testdata/packse/post/post-local-greater-than-post.toml @@ -0,0 +1,12 @@ +name = "post-local-greater-than-post" +description = "A greater-than version constraint should not match a post-release version with a local version identifier." + +[root] +requires = ["a>1.2.3.post1"] + +[expected] +satisfiable = false + +[packages.a.versions."1.2.3.post1"] + +[packages.a.versions."1.2.3.post1+local"] diff --git a/resolver/testdata/packse/post/post-local-greater-than.toml b/resolver/testdata/packse/post/post-local-greater-than.toml new file mode 100644 index 0000000..2c93a5d --- /dev/null +++ b/resolver/testdata/packse/post/post-local-greater-than.toml @@ -0,0 +1,12 @@ +name = "post-local-greater-than" +description = "A greater-than version constraint should not match a post-release version with a local version identifier." + +[root] +requires = ["a>1.2.3"] + +[expected] +satisfiable = false + +[packages.a.versions."1.2.3.post1"] + +[packages.a.versions."1.2.3.post1+local"] diff --git a/resolver/testdata/packse/post/post-simple.toml b/resolver/testdata/packse/post/post-simple.toml new file mode 100644 index 0000000..199c267 --- /dev/null +++ b/resolver/testdata/packse/post/post-simple.toml @@ -0,0 +1,10 @@ +name = "post-simple" +description = "A simple version constraint should not match a post-release version." + +[root] +requires = ["a==1.2.3"] + +[expected] +satisfiable = false + +[packages.a.versions."1.2.3.post1"] diff --git a/resolver/testdata/packse/prereleases/package-multiple-prereleases-kinds.toml b/resolver/testdata/packse/prereleases/package-multiple-prereleases-kinds.toml new file mode 100644 index 0000000..2819702 --- /dev/null +++ b/resolver/testdata/packse/prereleases/package-multiple-prereleases-kinds.toml @@ -0,0 +1,18 @@ +name = "package-multiple-prereleases-kinds" +description = "The user requires `a` which has multiple prereleases available with different labels." + +[root] +requires = ["a>=1.0.0a1"] + +[expected] +satisfiable = true +explanation = "Release candidates should be the highest precedence prerelease kind." + +[expected.packages] +a = "1.0.0rc1" + +[packages.a.versions."1.0.0a1"] + +[packages.a.versions."1.0.0b1"] + +[packages.a.versions."1.0.0rc1"] diff --git a/resolver/testdata/packse/prereleases/package-multiple-prereleases-numbers.toml b/resolver/testdata/packse/prereleases/package-multiple-prereleases-numbers.toml new file mode 100644 index 0000000..9570172 --- /dev/null +++ b/resolver/testdata/packse/prereleases/package-multiple-prereleases-numbers.toml @@ -0,0 +1,18 @@ +name = "package-multiple-prereleases-numbers" +description = "The user requires `a` which has multiple alphas available." + +[root] +requires = ["a>=1.0.0a1"] + +[expected] +satisfiable = true +explanation = "The latest alpha version should be selected." + +[expected.packages] +a = "1.0.0a3" + +[packages.a.versions."1.0.0a1"] + +[packages.a.versions."1.0.0a2"] + +[packages.a.versions."1.0.0a3"] diff --git a/resolver/testdata/packse/prereleases/package-only-prereleases-boundary.toml b/resolver/testdata/packse/prereleases/package-only-prereleases-boundary.toml new file mode 100644 index 0000000..bead9a5 --- /dev/null +++ b/resolver/testdata/packse/prereleases/package-only-prereleases-boundary.toml @@ -0,0 +1,18 @@ +name = "package-only-prereleases-boundary" +description = "The user requires a non-prerelease version of `a` which only has prerelease versions available. There are pre-releases on the boundary of their range." + +[root] +requires = ["a<0.2.0"] + +[expected] +satisfiable = true +explanation = "Since there are only prerelease versions of `a` available, a prerelease is allowed. Since the user did not explicitly request a pre-release, pre-releases at the boundary should not be selected." + +[expected.packages] +a = "0.1.0a1" + +[packages.a.versions."0.1.0a1"] + +[packages.a.versions."0.2.0a1"] + +[packages.a.versions."0.3.0a1"] diff --git a/resolver/testdata/packse/prereleases/package-only-prereleases-in-range.toml b/resolver/testdata/packse/prereleases/package-only-prereleases-in-range.toml new file mode 100644 index 0000000..df08ab7 --- /dev/null +++ b/resolver/testdata/packse/prereleases/package-only-prereleases-in-range.toml @@ -0,0 +1,13 @@ +name = "package-only-prereleases-in-range" +description = "The user requires a version of package `a` which only matches prerelease versions but they did not include a prerelease specifier." + +[root] +requires = ["a>0.1.0"] + +[expected] +satisfiable = false +explanation = "Since there are stable versions of `a` available, prerelease versions should not be selected without explicit opt-in." + +[packages.a.versions."0.1.0"] + +[packages.a.versions."1.0.0a1"] diff --git a/resolver/testdata/packse/prereleases/package-only-prereleases.toml b/resolver/testdata/packse/prereleases/package-only-prereleases.toml new file mode 100644 index 0000000..407deca --- /dev/null +++ b/resolver/testdata/packse/prereleases/package-only-prereleases.toml @@ -0,0 +1,14 @@ +name = "package-only-prereleases" +description = "The user requires any version of package `a` which only has prerelease versions available." + +[root] +requires = ["a"] + +[expected] +satisfiable = true +explanation = "Since there are only prerelease versions of `a` available, it should be installed even though the user did not include a prerelease specifier." + +[expected.packages] +a = "1.0.0a1" + +[packages.a.versions."1.0.0a1"] diff --git a/resolver/testdata/packse/prereleases/package-prerelease-specified-mixed-available.toml b/resolver/testdata/packse/prereleases/package-prerelease-specified-mixed-available.toml new file mode 100644 index 0000000..f7d4bc7 --- /dev/null +++ b/resolver/testdata/packse/prereleases/package-prerelease-specified-mixed-available.toml @@ -0,0 +1,20 @@ +name = "package-prerelease-specified-mixed-available" +description = "The user requires a version of `a` with a prerelease specifier and both prerelease and stable releases are available." + +[root] +requires = ["a>=0.1.0a1"] + +[expected] +satisfiable = true +explanation = "Since the user provided a prerelease specifier, the latest prerelease version should be selected." + +[expected.packages] +a = "1.0.0a1" + +[packages.a.versions."0.1.0"] + +[packages.a.versions."0.2.0a1"] + +[packages.a.versions."0.3.0"] + +[packages.a.versions."1.0.0a1"] diff --git a/resolver/testdata/packse/prereleases/package-prerelease-specified-only-final-available.toml b/resolver/testdata/packse/prereleases/package-prerelease-specified-only-final-available.toml new file mode 100644 index 0000000..97f7043 --- /dev/null +++ b/resolver/testdata/packse/prereleases/package-prerelease-specified-only-final-available.toml @@ -0,0 +1,18 @@ +name = "package-prerelease-specified-only-final-available" +description = "The user requires a version of `a` with a prerelease specifier and only stable releases are available." + +[root] +requires = ["a>=0.1.0a1"] + +[expected] +satisfiable = true +explanation = "The latest stable version should be selected." + +[expected.packages] +a = "0.3.0" + +[packages.a.versions."0.1.0"] + +[packages.a.versions."0.2.0"] + +[packages.a.versions."0.3.0"] diff --git a/resolver/testdata/packse/prereleases/package-prerelease-specified-only-prerelease-available.toml b/resolver/testdata/packse/prereleases/package-prerelease-specified-only-prerelease-available.toml new file mode 100644 index 0000000..8f1bcac --- /dev/null +++ b/resolver/testdata/packse/prereleases/package-prerelease-specified-only-prerelease-available.toml @@ -0,0 +1,18 @@ +name = "package-prerelease-specified-only-prerelease-available" +description = "The user requires a version of `a` with a prerelease specifier and only prerelease releases are available." + +[root] +requires = ["a>=0.1.0a1"] + +[expected] +satisfiable = true +explanation = "The latest prerelease version should be selected." + +[expected.packages] +a = "0.3.0a1" + +[packages.a.versions."0.1.0a1"] + +[packages.a.versions."0.2.0a1"] + +[packages.a.versions."0.3.0a1"] diff --git a/resolver/testdata/packse/prereleases/package-prereleases-boundary.toml b/resolver/testdata/packse/prereleases/package-prereleases-boundary.toml new file mode 100644 index 0000000..063c794 --- /dev/null +++ b/resolver/testdata/packse/prereleases/package-prereleases-boundary.toml @@ -0,0 +1,21 @@ +name = "package-prereleases-boundary" +description = "The user requires a non-prerelease version of `a` but has enabled pre-releases. There are pre-releases on the boundary of their range." + +[root] +requires = ["a<0.2.0"] + +[expected] +satisfiable = true +explanation = "Since the user did not use a pre-release specifier, pre-releases at the boundary should not be selected even though pre-releases are allowed." + +[expected.packages] +a = "0.1.0" + +[packages.a.versions."0.1.0"] + +[packages.a.versions."0.2.0a1"] + +[packages.a.versions."0.3.0"] + +[resolver_options] +prereleases = true diff --git a/resolver/testdata/packse/prereleases/package-prereleases-global-boundary.toml b/resolver/testdata/packse/prereleases/package-prereleases-global-boundary.toml new file mode 100644 index 0000000..0fba514 --- /dev/null +++ b/resolver/testdata/packse/prereleases/package-prereleases-global-boundary.toml @@ -0,0 +1,21 @@ +name = "package-prereleases-global-boundary" +description = "The user requires a non-prerelease version of `a` but has enabled pre-releases. There are pre-releases on the boundary of their range." + +[root] +requires = ["a<0.2.0"] + +[expected] +satisfiable = true +explanation = "Since the user did not use a pre-release specifier, pre-releases at the boundary should not be selected even though pre-releases are allowed." + +[expected.packages] +a = "0.1.0" + +[packages.a.versions."0.1.0"] + +[packages.a.versions."0.2.0a1"] + +[packages.a.versions."0.3.0"] + +[resolver_options] +prereleases = true diff --git a/resolver/testdata/packse/prereleases/package-prereleases-specifier-boundary.toml b/resolver/testdata/packse/prereleases/package-prereleases-specifier-boundary.toml new file mode 100644 index 0000000..c023fa8 --- /dev/null +++ b/resolver/testdata/packse/prereleases/package-prereleases-specifier-boundary.toml @@ -0,0 +1,24 @@ +name = "package-prereleases-specifier-boundary" +description = "The user requires a prerelease version of `a`. There are pre-releases on the boundary of their range." + +[root] +requires = ["a<0.2.0a2"] + +[expected] +satisfiable = true +explanation = "Since the user used a pre-release specifier, pre-releases at the boundary should be selected." + +[expected.packages] +a = "0.2.0a1" + +[packages.a.versions."0.1.0"] + +[packages.a.versions."0.2.0"] + +[packages.a.versions."0.2.0a1"] + +[packages.a.versions."0.2.0a2"] + +[packages.a.versions."0.2.0a3"] + +[packages.a.versions."0.3.0"] diff --git a/resolver/testdata/packse/prereleases/requires-package-only-prereleases-in-range-global-opt-in.toml b/resolver/testdata/packse/prereleases/requires-package-only-prereleases-in-range-global-opt-in.toml new file mode 100644 index 0000000..10a4874 --- /dev/null +++ b/resolver/testdata/packse/prereleases/requires-package-only-prereleases-in-range-global-opt-in.toml @@ -0,0 +1,18 @@ +name = "requires-package-only-prereleases-in-range-global-opt-in" +description = "The user requires a version of package `a` which only matches prerelease versions. They did not include a prerelease specifier for the package, but they opted into prereleases globally." + +[root] +requires = ["a>0.1.0"] + +[expected] +satisfiable = true + +[expected.packages] +a = "1.0.0a1" + +[packages.a.versions."0.1.0"] + +[packages.a.versions."1.0.0a1"] + +[resolver_options] +prereleases = true diff --git a/resolver/testdata/packse/prereleases/requires-package-prerelease-and-final-any.toml b/resolver/testdata/packse/prereleases/requires-package-prerelease-and-final-any.toml new file mode 100644 index 0000000..bff6d03 --- /dev/null +++ b/resolver/testdata/packse/prereleases/requires-package-prerelease-and-final-any.toml @@ -0,0 +1,16 @@ +name = "requires-package-prerelease-and-final-any" +description = "The user requires any version of package `a` has a prerelease version available and an older non-prerelease version." + +[root] +requires = ["a"] + +[expected] +satisfiable = true +explanation = "Since the user did not provide a prerelease specifier, the older stable version should be selected." + +[expected.packages] +a = "0.1.0" + +[packages.a.versions."0.1.0"] + +[packages.a.versions."1.0.0a1"] diff --git a/resolver/testdata/packse/prereleases/transitive-package-only-prereleases-in-range-opt-in.toml b/resolver/testdata/packse/prereleases/transitive-package-only-prereleases-in-range-opt-in.toml new file mode 100644 index 0000000..9abd287 --- /dev/null +++ b/resolver/testdata/packse/prereleases/transitive-package-only-prereleases-in-range-opt-in.toml @@ -0,0 +1,20 @@ +name = "transitive-package-only-prereleases-in-range-opt-in" +description = "The user requires package `a` which has a dependency on a package which only matches prerelease versions; the user has opted into allowing prereleases in `b` explicitly." + +[root] +requires = ["a", "b>0.0.0a1"] + +[expected] +satisfiable = true +explanation = "Since the user included a dependency on `b` with a prerelease specifier, a prerelease version can be selected." + +[expected.packages] +a = "0.1.0" +b = "1.0.0a1" + +[packages.a.versions."0.1.0"] +requires = ["b>0.1"] + +[packages.b.versions."0.1.0"] + +[packages.b.versions."1.0.0a1"] diff --git a/resolver/testdata/packse/prereleases/transitive-package-only-prereleases-in-range.toml b/resolver/testdata/packse/prereleases/transitive-package-only-prereleases-in-range.toml new file mode 100644 index 0000000..b6dd2ae --- /dev/null +++ b/resolver/testdata/packse/prereleases/transitive-package-only-prereleases-in-range.toml @@ -0,0 +1,16 @@ +name = "transitive-package-only-prereleases-in-range" +description = "The user requires package `a` which has a dependency on a package which only matches prerelease versions but they did not include a prerelease specifier." + +[root] +requires = ["a"] + +[expected] +satisfiable = false +explanation = "Since there are stable versions of `b` available, the prerelease version should not be selected without explicit opt-in. The available version is excluded by the range requested by the user." + +[packages.a.versions."0.1.0"] +requires = ["b>0.1"] + +[packages.b.versions."0.1.0"] + +[packages.b.versions."1.0.0a1"] diff --git a/resolver/testdata/packse/prereleases/transitive-package-only-prereleases.toml b/resolver/testdata/packse/prereleases/transitive-package-only-prereleases.toml new file mode 100644 index 0000000..6ab9c67 --- /dev/null +++ b/resolver/testdata/packse/prereleases/transitive-package-only-prereleases.toml @@ -0,0 +1,18 @@ +name = "transitive-package-only-prereleases" +description = "The user requires any version of package `a` which requires `b` which only has prerelease versions available." + +[root] +requires = ["a"] + +[expected] +satisfiable = true +explanation = "Since there are only prerelease versions of `b` available, it should be selected even though the user did not opt-in to prereleases." + +[expected.packages] +a = "0.1.0" +b = "1.0.0a1" + +[packages.a.versions."0.1.0"] +requires = ["b"] + +[packages.b.versions."1.0.0a1"] diff --git a/resolver/testdata/packse/prereleases/transitive-prerelease-and-stable-dependency-many-versions-holes.toml b/resolver/testdata/packse/prereleases/transitive-prerelease-and-stable-dependency-many-versions-holes.toml new file mode 100644 index 0000000..8d7b782 --- /dev/null +++ b/resolver/testdata/packse/prereleases/transitive-prerelease-and-stable-dependency-many-versions-holes.toml @@ -0,0 +1,53 @@ +name = "transitive-prerelease-and-stable-dependency-many-versions-holes" +description = "A transitive dependency has both a prerelease and a stable selector, but can only be satisfied by a prerelease. There are many prerelease versions and some are excluded." + +[root] +requires = ["a", "b"] + +[expected] +satisfiable = false +explanation = "Since the user did not explicitly opt-in to a prerelease, it cannot be selected." + +[packages.a.versions."1.0.0"] +requires = ["c>1.0.0,!=2.0.0a5,!=2.0.0a6,!=2.0.0a7,!=2.0.0b1,<2.0.0b5"] + +[packages.b.versions."1.0.0"] +requires = ["c>=1.0.0,<=3.0.0"] + +[packages.c.versions."1.0.0"] + +[packages.c.versions."2.0.0a1"] + +[packages.c.versions."2.0.0a2"] + +[packages.c.versions."2.0.0a3"] + +[packages.c.versions."2.0.0a4"] + +[packages.c.versions."2.0.0a5"] + +[packages.c.versions."2.0.0a6"] + +[packages.c.versions."2.0.0a7"] + +[packages.c.versions."2.0.0a8"] + +[packages.c.versions."2.0.0a9"] + +[packages.c.versions."2.0.0b1"] + +[packages.c.versions."2.0.0b2"] + +[packages.c.versions."2.0.0b3"] + +[packages.c.versions."2.0.0b4"] + +[packages.c.versions."2.0.0b5"] + +[packages.c.versions."2.0.0b6"] + +[packages.c.versions."2.0.0b7"] + +[packages.c.versions."2.0.0b8"] + +[packages.c.versions."2.0.0b9"] diff --git a/resolver/testdata/packse/prereleases/transitive-prerelease-and-stable-dependency-many-versions.toml b/resolver/testdata/packse/prereleases/transitive-prerelease-and-stable-dependency-many-versions.toml new file mode 100644 index 0000000..d6531c8 --- /dev/null +++ b/resolver/testdata/packse/prereleases/transitive-prerelease-and-stable-dependency-many-versions.toml @@ -0,0 +1,53 @@ +name = "transitive-prerelease-and-stable-dependency-many-versions" +description = "A transitive dependency has both a prerelease and a stable selector, but can only be satisfied by a prerelease. There are many prerelease versions." + +[root] +requires = ["a", "b"] + +[expected] +satisfiable = false +explanation = "Since the user did not explicitly opt-in to a prerelease, it cannot be selected." + +[packages.a.versions."1.0.0"] +requires = ["c>=2.0.0b1"] + +[packages.b.versions."1.0.0"] +requires = ["c>=1.0.0,<=3.0.0"] + +[packages.c.versions."1.0.0"] + +[packages.c.versions."2.0.0a1"] + +[packages.c.versions."2.0.0a2"] + +[packages.c.versions."2.0.0a3"] + +[packages.c.versions."2.0.0a4"] + +[packages.c.versions."2.0.0a5"] + +[packages.c.versions."2.0.0a6"] + +[packages.c.versions."2.0.0a7"] + +[packages.c.versions."2.0.0a8"] + +[packages.c.versions."2.0.0a9"] + +[packages.c.versions."2.0.0b1"] + +[packages.c.versions."2.0.0b2"] + +[packages.c.versions."2.0.0b3"] + +[packages.c.versions."2.0.0b4"] + +[packages.c.versions."2.0.0b5"] + +[packages.c.versions."2.0.0b6"] + +[packages.c.versions."2.0.0b7"] + +[packages.c.versions."2.0.0b8"] + +[packages.c.versions."2.0.0b9"] diff --git a/resolver/testdata/packse/prereleases/transitive-prerelease-and-stable-dependency-opt-in.toml b/resolver/testdata/packse/prereleases/transitive-prerelease-and-stable-dependency-opt-in.toml new file mode 100644 index 0000000..a2049b6 --- /dev/null +++ b/resolver/testdata/packse/prereleases/transitive-prerelease-and-stable-dependency-opt-in.toml @@ -0,0 +1,24 @@ +name = "transitive-prerelease-and-stable-dependency-opt-in" +description = "A transitive dependency has both a prerelease and a stable selector, but can only be satisfied by a prerelease. The user includes an opt-in to prereleases of the transitive dependency." + +[root] +requires = ["a", "b", "c>=0.0.0a1"] + +[expected] +satisfiable = true +explanation = "Since the user explicitly opted-in to a prerelease for `c`, it can be installed." + +[expected.packages] +a = "1.0.0" +b = "1.0.0" +c = "2.0.0b1" + +[packages.a.versions."1.0.0"] +requires = ["c==2.0.0b1"] + +[packages.b.versions."1.0.0"] +requires = ["c>=1.0.0,<=3.0.0"] + +[packages.c.versions."1.0.0"] + +[packages.c.versions."2.0.0b1"] diff --git a/resolver/testdata/packse/prereleases/transitive-prerelease-and-stable-dependency.toml b/resolver/testdata/packse/prereleases/transitive-prerelease-and-stable-dependency.toml new file mode 100644 index 0000000..fad7392 --- /dev/null +++ b/resolver/testdata/packse/prereleases/transitive-prerelease-and-stable-dependency.toml @@ -0,0 +1,19 @@ +name = "transitive-prerelease-and-stable-dependency" +description = "A transitive dependency has both a prerelease and a stable selector, but can only be satisfied by a prerelease" + +[root] +requires = ["a", "b"] + +[expected] +satisfiable = false +explanation = "Since the user did not explicitly opt-in to a prerelease, it cannot be selected." + +[packages.a.versions."1.0.0"] +requires = ["c==2.0.0b1"] + +[packages.b.versions."1.0.0"] +requires = ["c>=1.0.0,<=3.0.0"] + +[packages.c.versions."1.0.0"] + +[packages.c.versions."2.0.0b1"] diff --git a/resolver/testdata/packse/requires_python/compatible-python-incompatible-override.toml b/resolver/testdata/packse/requires_python/compatible-python-incompatible-override.toml new file mode 100644 index 0000000..6f26a62 --- /dev/null +++ b/resolver/testdata/packse/requires_python/compatible-python-incompatible-override.toml @@ -0,0 +1,17 @@ +name = "compatible-python-incompatible-override" +description = "The user requires a package which requires a compatible Python version, but they request an incompatible Python version for package resolution." + +[root] +requires = ["a==1.0.0"] + +[expected] +satisfiable = false + +[packages.a.versions."1.0.0"] +requires_python = ">=3.10" + +[resolver_options] +python = "3.9" + +[environment] +python = "3.11" diff --git a/resolver/testdata/packse/requires_python/incompatible-python-compatible-override-available-no-wheels.toml b/resolver/testdata/packse/requires_python/incompatible-python-compatible-override-available-no-wheels.toml new file mode 100644 index 0000000..dc08e4b --- /dev/null +++ b/resolver/testdata/packse/requires_python/incompatible-python-compatible-override-available-no-wheels.toml @@ -0,0 +1,23 @@ +name = "incompatible-python-compatible-override-available-no-wheels" +description = "The user requires a package which requires a incompatible Python version, but they request a compatible Python version for package resolution. There are only source distributions available for the package. The user has a compatible Python version installed elsewhere on their system." + +[root] +requires = ["a==1.0.0"] + +[expected] +satisfiable = true +explanation = "Since there is a compatible Python version available on the system, it should be used to build the source distributions." + +[expected.packages] +a = "1.0.0" + +[packages.a.versions."1.0.0"] +requires_python = ">=3.10" +wheel = false + +[resolver_options] +python = "3.11" + +[environment] +python = "3.9" +additional_python = ["3.11"] diff --git a/resolver/testdata/packse/requires_python/incompatible-python-compatible-override-no-compatible-wheels.toml b/resolver/testdata/packse/requires_python/incompatible-python-compatible-override-no-compatible-wheels.toml new file mode 100644 index 0000000..77359d7 --- /dev/null +++ b/resolver/testdata/packse/requires_python/incompatible-python-compatible-override-no-compatible-wheels.toml @@ -0,0 +1,19 @@ +name = "incompatible-python-compatible-override-no-compatible-wheels" +description = "The user requires a package which requires a incompatible Python version, but they request a compatible Python version for package resolution. There is a wheel available for the package, but it does not have a compatible tag." + +[root] +requires = ["a==1.0.0"] + +[expected] +satisfiable = true +explanation = "Since there are no compatible wheels for the package and it is not compatible with the local installation, we cannot build the source distribution to determine its dependencies. However, the source distribution includes static metadata, which we can use to determine dependencies without building the package." + +[packages.a.versions."1.0.0"] +requires_python = ">=3.10" +wheel_tags = ["foo-none-any"] + +[resolver_options] +python = "3.11" + +[environment] +python = "3.9" diff --git a/resolver/testdata/packse/requires_python/incompatible-python-compatible-override-other-wheel.toml b/resolver/testdata/packse/requires_python/incompatible-python-compatible-override-other-wheel.toml new file mode 100644 index 0000000..2698891 --- /dev/null +++ b/resolver/testdata/packse/requires_python/incompatible-python-compatible-override-other-wheel.toml @@ -0,0 +1,22 @@ +name = "incompatible-python-compatible-override-other-wheel" +description = "The user requires a package which requires a incompatible Python version, but they request a compatible Python version for package resolution. There are only source distributions available for the compatible version of the package, but there is an incompatible version with a wheel available." + +[root] +requires = ["a"] + +[expected] +satisfiable = true +explanation = "Since there are no wheels for the version of the package compatible with the target and it is not compatible with the local installation, we cannot build the source distribution to determine its dependencies. However, the source distribution includes static metadata, which we can use to determine dependencies without building the package." + +[packages.a.versions."1.0.0"] +requires_python = ">=3.10" +wheel = false + +[packages.a.versions."2.0.0"] +requires_python = ">=3.12" + +[resolver_options] +python = "3.11" + +[environment] +python = "3.9" diff --git a/resolver/testdata/packse/requires_python/incompatible-python-compatible-override-unavailable-no-wheels.toml b/resolver/testdata/packse/requires_python/incompatible-python-compatible-override-unavailable-no-wheels.toml new file mode 100644 index 0000000..24d8131 --- /dev/null +++ b/resolver/testdata/packse/requires_python/incompatible-python-compatible-override-unavailable-no-wheels.toml @@ -0,0 +1,19 @@ +name = "incompatible-python-compatible-override-unavailable-no-wheels" +description = "The user requires a package which requires a incompatible Python version, but they request a compatible Python version for package resolution. There are only source distributions available for the package." + +[root] +requires = ["a==1.0.0"] + +[expected] +satisfiable = true +explanation = "Since there are no wheels for the package and it is not compatible with the local installation, we cannot build the source distribution to determine its dependencies. However, the source distribution includes static metadata, which we can use to determine dependencies without building the package." + +[packages.a.versions."1.0.0"] +requires_python = ">=3.10" +wheel = false + +[resolver_options] +python = "3.11" + +[environment] +python = "3.9" diff --git a/resolver/testdata/packse/requires_python/incompatible-python-compatible-override.toml b/resolver/testdata/packse/requires_python/incompatible-python-compatible-override.toml new file mode 100644 index 0000000..9f4481f --- /dev/null +++ b/resolver/testdata/packse/requires_python/incompatible-python-compatible-override.toml @@ -0,0 +1,20 @@ +name = "incompatible-python-compatible-override" +description = "The user requires a package which requires a Python version greater than the current version, but they use an alternative Python version for package resolution." + +[root] +requires = ["a==1.0.0"] + +[expected] +satisfiable = true + +[expected.packages] +a = "1.0.0" + +[packages.a.versions."1.0.0"] +requires_python = ">=3.10" + +[resolver_options] +python = "3.11" + +[environment] +python = "3.9" diff --git a/resolver/testdata/packse/requires_python/python-greater-than-current-backtrack.toml b/resolver/testdata/packse/requires_python/python-greater-than-current-backtrack.toml new file mode 100644 index 0000000..492fabc --- /dev/null +++ b/resolver/testdata/packse/requires_python/python-greater-than-current-backtrack.toml @@ -0,0 +1,26 @@ +name = "python-greater-than-current-backtrack" +description = "The user requires a package where recent versions require a Python version greater than the current version, but an older version is compatible." + +[root] +requires = ["a"] + +[expected] +satisfiable = true + +[expected.packages] +a = "1.0.0" + +[packages.a.versions."1.0.0"] +requires_python = ">=3.9" + +[packages.a.versions."2.0.0"] +requires_python = ">=3.10" + +[packages.a.versions."3.0.0"] +requires_python = ">=3.11" + +[packages.a.versions."4.0.0"] +requires_python = ">=3.12" + +[environment] +python = "3.9" diff --git a/resolver/testdata/packse/requires_python/python-greater-than-current-excluded.toml b/resolver/testdata/packse/requires_python/python-greater-than-current-excluded.toml new file mode 100644 index 0000000..97e3a1e --- /dev/null +++ b/resolver/testdata/packse/requires_python/python-greater-than-current-excluded.toml @@ -0,0 +1,23 @@ +name = "python-greater-than-current-excluded" +description = "The user requires a package where recent versions require a Python version greater than the current version, but an excluded older version is compatible." + +[root] +requires = ["a>=2.0.0"] + +[expected] +satisfiable = false + +[packages.a.versions."1.0.0"] +requires_python = ">=3.9" + +[packages.a.versions."2.0.0"] +requires_python = ">=3.10" + +[packages.a.versions."3.0.0"] +requires_python = ">=3.11" + +[packages.a.versions."4.0.0"] +requires_python = ">=3.12" + +[environment] +python = "3.9" diff --git a/resolver/testdata/packse/requires_python/python-greater-than-current-many.toml b/resolver/testdata/packse/requires_python/python-greater-than-current-many.toml new file mode 100644 index 0000000..1d5661e --- /dev/null +++ b/resolver/testdata/packse/requires_python/python-greater-than-current-many.toml @@ -0,0 +1,47 @@ +name = "python-greater-than-current-many" +description = "The user requires a package which has many versions which all require a Python version greater than the current version" + +[root] +requires = ["a==1.0.0"] + +[expected] +satisfiable = false + +[packages.a.versions."2.0.0"] +requires_python = ">=3.10" + +[packages.a.versions."2.1.0"] +requires_python = ">=3.10" + +[packages.a.versions."2.2.0"] +requires_python = ">=3.10" + +[packages.a.versions."2.3.0"] +requires_python = ">=3.10" + +[packages.a.versions."2.4.0"] +requires_python = ">=3.10" + +[packages.a.versions."2.5.0"] +requires_python = ">=3.10" + +[packages.a.versions."3.0.0"] +requires_python = ">=3.11" + +[packages.a.versions."3.1.0"] +requires_python = ">=3.11" + +[packages.a.versions."3.2.0"] +requires_python = ">=3.11" + +[packages.a.versions."3.3.0"] +requires_python = ">=3.11" + +[packages.a.versions."3.4.0"] +requires_python = ">=3.11" + +[packages.a.versions."3.5.0"] +requires_python = ">=3.11" + +[environment] +python = "3.9" diff --git a/resolver/testdata/packse/requires_python/python-greater-than-current-patch.toml b/resolver/testdata/packse/requires_python/python-greater-than-current-patch.toml new file mode 100644 index 0000000..ed189eb --- /dev/null +++ b/resolver/testdata/packse/requires_python/python-greater-than-current-patch.toml @@ -0,0 +1,14 @@ +name = "python-greater-than-current-patch" +description = "The user requires a package which requires a Python version with a patch version greater than the current patch version" + +[root] +requires = ["a==1.0.0"] + +[expected] +satisfiable = false + +[packages.a.versions."1.0.0"] +requires_python = ">=3.13.2" + +[environment] +python = "3.13.0" diff --git a/resolver/testdata/packse/requires_python/python-greater-than-current.toml b/resolver/testdata/packse/requires_python/python-greater-than-current.toml new file mode 100644 index 0000000..d3e0c00 --- /dev/null +++ b/resolver/testdata/packse/requires_python/python-greater-than-current.toml @@ -0,0 +1,14 @@ +name = "python-greater-than-current" +description = "The user requires a package which requires a Python version greater than the current version" + +[root] +requires = ["a==1.0.0"] + +[expected] +satisfiable = false + +[packages.a.versions."1.0.0"] +requires_python = ">=3.10" + +[environment] +python = "3.9" diff --git a/resolver/testdata/packse/requires_python/python-less-than-current.toml b/resolver/testdata/packse/requires_python/python-less-than-current.toml new file mode 100644 index 0000000..80c34ba --- /dev/null +++ b/resolver/testdata/packse/requires_python/python-less-than-current.toml @@ -0,0 +1,15 @@ +name = "python-less-than-current" +description = "The user requires a package which requires a Python version less than the current version" + +[root] +requires = ["a==1.0.0"] + +[expected] +satisfiable = true +explanation = "We ignore the upper bound on Python requirements" + +[packages.a.versions."1.0.0"] +requires_python = "<=3.8" + +[environment] +python = "3.9" diff --git a/resolver/testdata/packse/requires_python/python-patch-override-no-patch.toml b/resolver/testdata/packse/requires_python/python-patch-override-no-patch.toml new file mode 100644 index 0000000..395c109 --- /dev/null +++ b/resolver/testdata/packse/requires_python/python-patch-override-no-patch.toml @@ -0,0 +1,18 @@ +name = "python-patch-override-no-patch" +description = "The user requires a package which requires a Python version with a patch version and the user provides a target version without a patch version." + +[root] +requires = ["a==1.0.0"] + +[expected] +satisfiable = false +explanation = "Since the resolver is asked to solve with 3.9, the minimum compatible Python requirement is treated as 3.9.0." + +[packages.a.versions."1.0.0"] +requires_python = ">=3.9.4" + +[resolver_options] +python = "3.9" + +[environment] +python = "3.9.21" diff --git a/resolver/testdata/packse/requires_python/python-patch-override-patch-compatible.toml b/resolver/testdata/packse/requires_python/python-patch-override-patch-compatible.toml new file mode 100644 index 0000000..fd7bd20 --- /dev/null +++ b/resolver/testdata/packse/requires_python/python-patch-override-patch-compatible.toml @@ -0,0 +1,20 @@ +name = "python-patch-override-patch-compatible" +description = "The user requires a package which requires a Python version with a patch version and the user provides a target version with a compatible patch version." + +[root] +requires = ["a==1.0.0"] + +[expected] +satisfiable = true + +[expected.packages] +a = "1.0.0" + +[packages.a.versions."1.0.0"] +requires_python = ">=3.9.0" + +[resolver_options] +python = "3.9.0" + +[environment] +python = "3.9.21" diff --git a/resolver/testdata/packse/requires_python/python-version-does-not-exist.toml b/resolver/testdata/packse/requires_python/python-version-does-not-exist.toml new file mode 100644 index 0000000..da7219c --- /dev/null +++ b/resolver/testdata/packse/requires_python/python-version-does-not-exist.toml @@ -0,0 +1,11 @@ +name = "python-version-does-not-exist" +description = "The user requires a package which requires a Python version that does not exist" + +[root] +requires = ["a==1.0.0"] + +[expected] +satisfiable = false + +[packages.a.versions."1.0.0"] +requires_python = ">=3.30" diff --git a/resolver/testdata/packse/tag_and_markers/requires-python-wheels.toml b/resolver/testdata/packse/tag_and_markers/requires-python-wheels.toml new file mode 100644 index 0000000..4c8e7e3 --- /dev/null +++ b/resolver/testdata/packse/tag_and_markers/requires-python-wheels.toml @@ -0,0 +1,19 @@ +name = "requires-python-wheels" +description = "Check that we only include wheels that match the required Python version" + +[root] +requires_python = ">=3.10" +requires = ["a==1.0.0"] + +[expected] +satisfiable = true + +[packages.a.versions."1.0.0"] +requires_python = ">=3.10" +wheel_tags = ["cp311-cp311-any", "cp310-cp310-any", "cp39-cp39-any"] + +[resolver_options] +universal = true + +[environment] +python = "3.12" diff --git a/resolver/testdata/packse/tag_and_markers/unreachable-package.toml b/resolver/testdata/packse/tag_and_markers/unreachable-package.toml new file mode 100644 index 0000000..6554650 --- /dev/null +++ b/resolver/testdata/packse/tag_and_markers/unreachable-package.toml @@ -0,0 +1,16 @@ +name = "unreachable-package" +description = "`c` is not reachable due to the markers, it should be excluded from the lockfile" + +[root] +requires = ["a==1.0.0; sys_platform == 'win32'"] + +[expected] +satisfiable = true + +[packages.a.versions."1.0.0"] +requires = ["b==1.0.0; sys_platform == 'linux'"] + +[packages.b.versions."1.0.0"] + +[resolver_options] +universal = true diff --git a/resolver/testdata/packse/tag_and_markers/unreachable-wheels.toml b/resolver/testdata/packse/tag_and_markers/unreachable-wheels.toml new file mode 100644 index 0000000..1c72792 --- /dev/null +++ b/resolver/testdata/packse/tag_and_markers/unreachable-wheels.toml @@ -0,0 +1,39 @@ +name = "unreachable-wheels" +description = "Check that we only include wheels that match the platform markers" + +[root] +requires = [ + "a==1.0.0; sys_platform == 'win32'", + "b==1.0.0; sys_platform == 'linux'", + "c==1.0.0; sys_platform == 'darwin'", +] + +[expected] +satisfiable = true + +[packages.a.versions."1.0.0"] +wheel_tags = [ + "cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64", + "cp312-cp312-musllinux_1_1_armv7l", + "cp312-cp312-win_amd64", + "cp312-cp312-macosx_14_0_x86_64", +] + +[packages.b.versions."1.0.0"] +wheel_tags = [ + "cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64", + "cp312-cp312-musllinux_1_1_armv7l", + "cp312-cp312-win_amd64", + "cp312-cp312-macosx_14_0_x86_64", +] + +[packages.c.versions."1.0.0"] +wheel_tags = [ + "cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64", + "cp312-cp312-musllinux_1_1_armv7l", + "cp312-cp312-win_amd64", + "cp312-cp312-macosx_14_0_x86_64", +] + +[resolver_options] +universal = true diff --git a/resolver/testdata/packse/tag_and_markers/virtual-package-extra-priorities.toml b/resolver/testdata/packse/tag_and_markers/virtual-package-extra-priorities.toml new file mode 100644 index 0000000..220a191 --- /dev/null +++ b/resolver/testdata/packse/tag_and_markers/virtual-package-extra-priorities.toml @@ -0,0 +1,26 @@ +name = "marker-variants-have-different-extras" +description = "Check the prioritization for virtual extra and marker packages" + +[expected] +satisfiable = true + +[environment] +python = "3.12" + +[root] +requires = [ + "psycopg[binary] ; platform_python_implementation != 'PyPy'", + "psycopg ; platform_python_implementation == 'PyPy'", +] + +[packages.psycopg.versions."1.0.0"] +requires = ['tzdata; sys_platform == "win32"'] + +[packages.psycopg.versions."1.0.0".extras] +binary = ["psycopg-binary; implementation_name != 'pypy'"] + +[packages.psycopg-binary.versions."1.0.0"] +[packages.tzdata.versions."1.0.0"] + +[resolver_options] +universal = true diff --git a/resolver/testdata/packse/tag_and_markers/virtual-package-marker-priorities.toml b/resolver/testdata/packse/tag_and_markers/virtual-package-marker-priorities.toml new file mode 100644 index 0000000..51aae43 --- /dev/null +++ b/resolver/testdata/packse/tag_and_markers/virtual-package-marker-priorities.toml @@ -0,0 +1,23 @@ +name = "virtual-package-extra-priorities" +description = "Check the prioritization for virtual marker packages" + +[root] +requires_python = ">=3.12" +requires = ["a==1; python_version >= '3.8'", "b; python_version >= '3.9'"] + +[expected] +satisfiable = true + +[packages.a.versions."1.0.0"] +requires = ["b==1 ; python_version >= '3.10'"] +[packages.a.versions."2.0.0"] +requires = ["b==1 ; python_version >= '3.10'"] + +[packages.b.versions."1.0.0"] +[packages.b.versions."2.0.0"] + +[resolver_options] +universal = true + +[environment] +python = "3.12" diff --git a/resolver/testdata/packse/wheels/no-binary.toml b/resolver/testdata/packse/wheels/no-binary.toml new file mode 100644 index 0000000..375b130 --- /dev/null +++ b/resolver/testdata/packse/wheels/no-binary.toml @@ -0,0 +1,14 @@ +name = "no-binary" +description = "Both wheels and source distributions are available, and the user has disabled binaries." + +[root] +requires = ["a"] + +[expected] +satisfiable = true +explanation = "The source distribution should be used for install" + +[packages.a.versions."1.0.0"] + +[resolver_options] +no_binary = ["a"] diff --git a/resolver/testdata/packse/wheels/no-build.toml b/resolver/testdata/packse/wheels/no-build.toml new file mode 100644 index 0000000..f1a082f --- /dev/null +++ b/resolver/testdata/packse/wheels/no-build.toml @@ -0,0 +1,14 @@ +name = "no-build" +description = "Both wheels and source distributions are available, and the user has disabled builds." + +[root] +requires = ["a"] + +[expected] +satisfiable = true +explanation = "The wheel should be used for install" + +[packages.a.versions."1.0.0"] + +[resolver_options] +no_build = ["a"] diff --git a/resolver/testdata/packse/wheels/no-sdist-no-wheels-with-matching-abi.toml b/resolver/testdata/packse/wheels/no-sdist-no-wheels-with-matching-abi.toml new file mode 100644 index 0000000..73a5a4f --- /dev/null +++ b/resolver/testdata/packse/wheels/no-sdist-no-wheels-with-matching-abi.toml @@ -0,0 +1,15 @@ +name = "no-sdist-no-wheels-with-matching-abi" +description = "No wheels with matching ABI tags are available, nor are any source distributions available" + +[root] +requires = ["a"] + +[expected] +satisfiable = false + +[packages.a.versions."1.0.0"] +wheel_tags = ["py3-graalpy240_310_native-any"] +sdist = false + +[resolver_options] +python_platform = "x86_64-manylinux2014" diff --git a/resolver/testdata/packse/wheels/no-sdist-no-wheels-with-matching-platform.toml b/resolver/testdata/packse/wheels/no-sdist-no-wheels-with-matching-platform.toml new file mode 100644 index 0000000..35ddcaa --- /dev/null +++ b/resolver/testdata/packse/wheels/no-sdist-no-wheels-with-matching-platform.toml @@ -0,0 +1,15 @@ +name = "no-sdist-no-wheels-with-matching-platform" +description = "No wheels with matching platform tags are available, nor are any source distributions available" + +[root] +requires = ["a"] + +[expected] +satisfiable = false + +[packages.a.versions."1.0.0"] +wheel_tags = ["py3-none-macosx_10_0_ppc64"] +sdist = false + +[resolver_options] +python_platform = "x86_64-manylinux2014" diff --git a/resolver/testdata/packse/wheels/no-sdist-no-wheels-with-matching-python.toml b/resolver/testdata/packse/wheels/no-sdist-no-wheels-with-matching-python.toml new file mode 100644 index 0000000..9b5d28c --- /dev/null +++ b/resolver/testdata/packse/wheels/no-sdist-no-wheels-with-matching-python.toml @@ -0,0 +1,15 @@ +name = "no-sdist-no-wheels-with-matching-python" +description = "No wheels with matching Python tags are available, nor are any source distributions available" + +[root] +requires = ["a"] + +[expected] +satisfiable = false + +[packages.a.versions."1.0.0"] +wheel_tags = ["graalpy310-none-any"] +sdist = false + +[resolver_options] +python_platform = "x86_64-manylinux2014" diff --git a/resolver/testdata/packse/wheels/no-wheels-no-build.toml b/resolver/testdata/packse/wheels/no-wheels-no-build.toml new file mode 100644 index 0000000..ac07da4 --- /dev/null +++ b/resolver/testdata/packse/wheels/no-wheels-no-build.toml @@ -0,0 +1,14 @@ +name = "no-wheels-no-build" +description = "No wheels are available, only source distributions but the user has disabled builds." + +[root] +requires = ["a"] + +[expected] +satisfiable = false + +[packages.a.versions."1.0.0"] +wheel = false + +[resolver_options] +no_build = ["a"] diff --git a/resolver/testdata/packse/wheels/no-wheels-with-matching-platform.toml b/resolver/testdata/packse/wheels/no-wheels-with-matching-platform.toml new file mode 100644 index 0000000..d24d42f --- /dev/null +++ b/resolver/testdata/packse/wheels/no-wheels-with-matching-platform.toml @@ -0,0 +1,11 @@ +name = "no-wheels-with-matching-platform" +description = "No wheels with matching platform tags are available, just source distributions." + +[root] +requires = ["a"] + +[expected] +satisfiable = true + +[packages.a.versions."1.0.0"] +wheel_tags = ["py3-any-macosx_10_0_ppc64"] diff --git a/resolver/testdata/packse/wheels/no-wheels.toml b/resolver/testdata/packse/wheels/no-wheels.toml new file mode 100644 index 0000000..235eeee --- /dev/null +++ b/resolver/testdata/packse/wheels/no-wheels.toml @@ -0,0 +1,11 @@ +name = "no-wheels" +description = "No wheels are available, only source distributions." + +[root] +requires = ["a"] + +[expected] +satisfiable = true + +[packages.a.versions."1.0.0"] +wheel = false diff --git a/resolver/testdata/packse/wheels/only-wheels-no-binary.toml b/resolver/testdata/packse/wheels/only-wheels-no-binary.toml new file mode 100644 index 0000000..1076721 --- /dev/null +++ b/resolver/testdata/packse/wheels/only-wheels-no-binary.toml @@ -0,0 +1,14 @@ +name = "only-wheels-no-binary" +description = "No source distributions are available, only wheels but the user has disabled using pre-built binaries." + +[root] +requires = ["a"] + +[expected] +satisfiable = false + +[packages.a.versions."1.0.0"] +sdist = false + +[resolver_options] +no_binary = ["a"] diff --git a/resolver/testdata/packse/wheels/only-wheels.toml b/resolver/testdata/packse/wheels/only-wheels.toml new file mode 100644 index 0000000..cc66787 --- /dev/null +++ b/resolver/testdata/packse/wheels/only-wheels.toml @@ -0,0 +1,11 @@ +name = "only-wheels" +description = "No source distributions are available, only wheels." + +[root] +requires = ["a"] + +[expected] +satisfiable = true + +[packages.a.versions."1.0.0"] +sdist = false diff --git a/resolver/testdata/packse/wheels/requires-python-subset.toml b/resolver/testdata/packse/wheels/requires-python-subset.toml new file mode 100644 index 0000000..2d5e92e --- /dev/null +++ b/resolver/testdata/packse/wheels/requires-python-subset.toml @@ -0,0 +1,16 @@ +name = "requires-python-subset" +description = "While both Linux and Windows are required and `win-only` has only a Windows wheel, `win-only` is also used only on Windows." + +[resolver_options] +universal = true +required_environments = ['sys_platform == "linux"', 'sys_platform == "win32"'] + +[root] +requires = ["win-only; sys_platform == 'win32'"] + +[expected] +satisfiable = true + +[packages.win-only.versions."1.0.0"] +sdist = false +wheel_tags = ["cp312-abi3-win_amd64"] diff --git a/resolver/testdata/packse/wheels/specific-architecture.toml b/resolver/testdata/packse/wheels/specific-architecture.toml new file mode 100644 index 0000000..13443f7 --- /dev/null +++ b/resolver/testdata/packse/wheels/specific-architecture.toml @@ -0,0 +1,54 @@ +name = "specific-architecture" +description = "When a dependency is only required on a specific platform (like x86_64), omit wheels that target other platforms (like aarch64)." + +[root] +requires = ["a"] + +[expected] +satisfiable = true + +[packages.a.versions."1.0.0"] +requires = [ + "b ; platform_machine == 'x86_64'", + "c ; platform_machine == 'aarch64'", + "d ; platform_machine == 'i686'", +] + +[packages.b.versions."1.0.0"] +wheel_tags = [ + "cp313-cp313-manylinux2010_x86_64", + "cp313-cp313-manylinux2010_aarch64", + "cp313-cp313-manylinux2010_i686", + "cp313-cp313-freebsd_13_x86_64", + "cp313-cp313-freebsd_13_aarch64", + "cp313-cp313-macosx_10_9_x86_64", + "cp313-cp313-macosx_10_9_arm64", +] +sdist = false + +[packages.c.versions."1.0.0"] +wheel_tags = [ + "cp313-cp313-manylinux2010_x86_64", + "cp313-cp313-manylinux2010_aarch64", + "cp313-cp313-manylinux2010_i686", + "cp313-cp313-freebsd_13_x86_64", + "cp313-cp313-freebsd_13_aarch64", + "cp313-cp313-macosx_10_9_x86_64", + "cp313-cp313-macosx_10_9_arm64", +] +sdist = false + +[packages.d.versions."1.0.0"] +wheel_tags = [ + "cp313-cp313-manylinux2010_x86_64", + "cp313-cp313-manylinux2010_aarch64", + "cp313-cp313-manylinux2010_i686", + "cp313-cp313-freebsd_13_x86_64", + "cp313-cp313-freebsd_13_aarch64", + "cp313-cp313-macosx_10_9_x86_64", + "cp313-cp313-macosx_10_9_arm64", +] +sdist = false + +[resolver_options] +universal = true diff --git a/resolver/testdata/packse/wheels/specific-tag-and-default.toml b/resolver/testdata/packse/wheels/specific-tag-and-default.toml new file mode 100644 index 0000000..c2eb78a --- /dev/null +++ b/resolver/testdata/packse/wheels/specific-tag-and-default.toml @@ -0,0 +1,14 @@ +name = "specific-tag-and-default" +description = "A wheel for a specific platform is available alongside the default." + +[root] +requires = ["a"] + +[expected] +satisfiable = true + +[packages.a.versions."1.0.0"] +wheel_tags = [ + "cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64", + "py3-none-any", +] diff --git a/resolver/testdata/packse/yanked/package-only-yanked-in-range.toml b/resolver/testdata/packse/yanked/package-only-yanked-in-range.toml new file mode 100644 index 0000000..b529642 --- /dev/null +++ b/resolver/testdata/packse/yanked/package-only-yanked-in-range.toml @@ -0,0 +1,14 @@ +name = "package-only-yanked-in-range" +description = "The user requires a version of package `a` which only matches yanked versions." + +[root] +requires = ["a>0.1.0"] + +[expected] +satisfiable = false +explanation = "Since there are other versions of `a` available, yanked versions should not be selected without explicit opt-in." + +[packages.a.versions."0.1.0"] + +[packages.a.versions."1.0.0"] +yanked = true diff --git a/resolver/testdata/packse/yanked/package-only-yanked.toml b/resolver/testdata/packse/yanked/package-only-yanked.toml new file mode 100644 index 0000000..c39abe8 --- /dev/null +++ b/resolver/testdata/packse/yanked/package-only-yanked.toml @@ -0,0 +1,12 @@ +name = "package-only-yanked" +description = "The user requires any version of package `a` which only has yanked versions available." + +[root] +requires = ["a"] + +[expected] +satisfiable = false +explanation = "Yanked versions should not be installed, even if they are the only one available." + +[packages.a.versions."1.0.0"] +yanked = true diff --git a/resolver/testdata/packse/yanked/package-yanked-specified-mixed-available.toml b/resolver/testdata/packse/yanked/package-yanked-specified-mixed-available.toml new file mode 100644 index 0000000..3577b02 --- /dev/null +++ b/resolver/testdata/packse/yanked/package-yanked-specified-mixed-available.toml @@ -0,0 +1,22 @@ +name = "package-yanked-specified-mixed-available" +description = "The user requires any version of `a` and both yanked and unyanked releases are available." + +[root] +requires = ["a>=0.1.0"] + +[expected] +satisfiable = true +explanation = "The latest unyanked version should be selected." + +[expected.packages] +a = "0.3.0" + +[packages.a.versions."0.1.0"] + +[packages.a.versions."0.2.0"] +yanked = true + +[packages.a.versions."0.3.0"] + +[packages.a.versions."1.0.0"] +yanked = true diff --git a/resolver/testdata/packse/yanked/requires-package-yanked-and-unyanked-any.toml b/resolver/testdata/packse/yanked/requires-package-yanked-and-unyanked-any.toml new file mode 100644 index 0000000..c65b685 --- /dev/null +++ b/resolver/testdata/packse/yanked/requires-package-yanked-and-unyanked-any.toml @@ -0,0 +1,17 @@ +name = "requires-package-yanked-and-unyanked-any" +description = "The user requires any version of package `a` has a yanked version available and an older unyanked version." + +[root] +requires = ["a"] + +[expected] +satisfiable = true +explanation = "The unyanked version should be selected." + +[expected.packages] +a = "0.1.0" + +[packages.a.versions."0.1.0"] + +[packages.a.versions."1.0.0"] +yanked = true diff --git a/resolver/testdata/packse/yanked/transitive-package-only-yanked-in-range-opt-in.toml b/resolver/testdata/packse/yanked/transitive-package-only-yanked-in-range-opt-in.toml new file mode 100644 index 0000000..19d0fbb --- /dev/null +++ b/resolver/testdata/packse/yanked/transitive-package-only-yanked-in-range-opt-in.toml @@ -0,0 +1,21 @@ +name = "transitive-package-only-yanked-in-range-opt-in" +description = "The user requires package `a` which has a dependency on a package which only matches yanked versions; the user has opted into allowing the yanked version of `b` explicitly." + +[root] +requires = ["a", "b==1.0.0"] + +[expected] +satisfiable = true +explanation = "Since the user included a dependency on `b` with an exact specifier, the yanked version can be selected." + +[expected.packages] +a = "0.1.0" +b = "1.0.0" + +[packages.a.versions."0.1.0"] +requires = ["b>0.1"] + +[packages.b.versions."0.1.0"] + +[packages.b.versions."1.0.0"] +yanked = true diff --git a/resolver/testdata/packse/yanked/transitive-package-only-yanked-in-range.toml b/resolver/testdata/packse/yanked/transitive-package-only-yanked-in-range.toml new file mode 100644 index 0000000..14933eb --- /dev/null +++ b/resolver/testdata/packse/yanked/transitive-package-only-yanked-in-range.toml @@ -0,0 +1,17 @@ +name = "transitive-package-only-yanked-in-range" +description = "The user requires package `a` which has a dependency on a package which only matches yanked versions." + +[root] +requires = ["a"] + +[expected] +satisfiable = false +explanation = "Yanked versions should not be installed, even if they are the only valid version in a range." + +[packages.a.versions."0.1.0"] +requires = ["b>0.1"] + +[packages.b.versions."0.1.0"] + +[packages.b.versions."1.0.0"] +yanked = true diff --git a/resolver/testdata/packse/yanked/transitive-package-only-yanked.toml b/resolver/testdata/packse/yanked/transitive-package-only-yanked.toml new file mode 100644 index 0000000..f91564e --- /dev/null +++ b/resolver/testdata/packse/yanked/transitive-package-only-yanked.toml @@ -0,0 +1,15 @@ +name = "transitive-package-only-yanked" +description = "The user requires any version of package `a` which requires `b` which only has yanked versions available." + +[root] +requires = ["a"] + +[expected] +satisfiable = false +explanation = "Yanked versions should not be installed, even if they are the only one available." + +[packages.a.versions."0.1.0"] +requires = ["b"] + +[packages.b.versions."1.0.0"] +yanked = true diff --git a/resolver/testdata/packse/yanked/transitive-yanked-and-unyanked-dependency-opt-in.toml b/resolver/testdata/packse/yanked/transitive-yanked-and-unyanked-dependency-opt-in.toml new file mode 100644 index 0000000..ed38f31 --- /dev/null +++ b/resolver/testdata/packse/yanked/transitive-yanked-and-unyanked-dependency-opt-in.toml @@ -0,0 +1,25 @@ +name = "transitive-yanked-and-unyanked-dependency-opt-in" +description = "A transitive dependency has both a yanked and an unyanked version, but can only be satisfied by a yanked. The user includes an opt-in to the yanked version of the transitive dependency." + +[root] +requires = ["a", "b", "c==2.0.0"] + +[expected] +satisfiable = true +explanation = "Since the user explicitly selected the yanked version of `c`, it can be installed." + +[expected.packages] +a = "1.0.0" +b = "1.0.0" +c = "2.0.0" + +[packages.a.versions."1.0.0"] +requires = ["c==2.0.0"] + +[packages.b.versions."1.0.0"] +requires = ["c>=1.0.0,<=3.0.0"] + +[packages.c.versions."1.0.0"] + +[packages.c.versions."2.0.0"] +yanked = true diff --git a/resolver/testdata/packse/yanked/transitive-yanked-and-unyanked-dependency.toml b/resolver/testdata/packse/yanked/transitive-yanked-and-unyanked-dependency.toml new file mode 100644 index 0000000..9be759f --- /dev/null +++ b/resolver/testdata/packse/yanked/transitive-yanked-and-unyanked-dependency.toml @@ -0,0 +1,20 @@ +name = "transitive-yanked-and-unyanked-dependency" +description = "A transitive dependency has both a yanked and an unyanked version, but can only be satisfied by a yanked version" + +[root] +requires = ["a", "b"] + +[expected] +satisfiable = false +explanation = "Since the user did not explicitly select the yanked version, it cannot be used." + +[packages.a.versions."1.0.0"] +requires = ["c==2.0.0"] + +[packages.b.versions."1.0.0"] +requires = ["c>=1.0.0,<=3.0.0"] + +[packages.c.versions."1.0.0"] + +[packages.c.versions."2.0.0"] +yanked = true From 45454ac764c312038d7af2121a9c71235656f612 Mon Sep 17 00:00:00 2001 From: Jonathan Yoder Date: Thu, 24 Sep 2026 11:03:21 -0400 Subject: [PATCH 2/2] test(resolver): tighten review nits in the packse harness Match exactPinAdmitsYanked's yanked exact-pin check on the parsed requirement name instead of a raw substring, closing a false-positive risk from a prefixed package name. Guard buildOracleModel's final conjunction against zero clauses (unreachable today, but the same gophersat empty-AND footgun as atMostOne). Drop an unused binding. Refs rstudio/package-manager#18658 Co-Authored-By: Claude Opus 5.5 (1M context) --- resolver/packse_test.go | 3 +-- resolver/satoracle_test.go | 28 +++++++++++++++++++++------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/resolver/packse_test.go b/resolver/packse_test.go index 6706e3c..370c37f 100644 --- a/resolver/packse_test.go +++ b/resolver/packse_test.go @@ -203,8 +203,7 @@ func TestPackse(t *testing.T) { continue } - if reason, ok := unsupportedOption[name]; ok { - _ = reason + if _, ok := unsupportedOption[name]; ok { unsupported++ continue } diff --git a/resolver/satoracle_test.go b/resolver/satoracle_test.go index 29dafe3..b0f7310 100644 --- a/resolver/satoracle_test.go +++ b/resolver/satoracle_test.go @@ -47,15 +47,22 @@ type oracleVersion struct { // exactPinAdmitsYanked reports whether some root requirement pins pkgName to // exactly v, which is PEP 592's exception to "never offer a yanked release": -// an installer MAY still honor an explicit, exact request for it. Checked -// against the raw requirement text rather than a general specifier-shape -// analysis -- sufficient for the vendored corpus (measured: both scenarios -// that need this write it as "name==x.y.z"), and simpler than reconstructing -// "is this specifier exactly one version" from a parsed Specifiers. +// an installer MAY still honor an explicit, exact request for it. Checked by +// parsing each root requirement rather than a general specifier-shape +// analysis of Specifiers -- sufficient for the vendored corpus (measured: both +// scenarios that need this write it as "name==x.y.z"), and simpler than +// reconstructing "is this specifier exactly one version" from a parsed +// Specifiers. Matching on the parsed requirement name (not a substring of the +// raw text) avoids a false positive from a prefixed name, e.g. "bb==1.0.0" +// containing the text "b==1.0.0". func exactPinAdmitsYanked(root tomlRoot, pkgName string, v version.Version) bool { - needle := pkgName + "==" + v.String() + needle := "==" + v.String() for _, raw := range root.Requires { - if strings.Contains(raw, needle) { + req, err := requirement.Parse(raw) + if err != nil || req.Name != pkgName { + continue + } + if req.Specifiers.String() == needle { return true } } @@ -277,6 +284,13 @@ func buildOracleModel(t *testing.T, s tomlScenario, py pythonSpec, env marker.En } } + if len(clauses) == 0 { + // No root requirements and no packages: trivially satisfiable. Must be + // bf.True, not bf.And() with zero arguments -- see atMostOne's comment. + // Unreachable against the vendored corpus (every scenario has a non-empty + // root.requires), kept as a guard against a future re-pull that adds one. + return oracleModel{formula: bf.True, vars: allVars} + } return oracleModel{formula: bf.And(clauses...), vars: allVars} }