Skip to content

Commit

Permalink
Add forking scenarios that are unstable with preferences (#203)
Browse files Browse the repository at this point in the history
In the first scenario, `preferences-dependent-forking`, we show how the
resolution can be different the second time because we don't fork
anymore because the preferences from the lockfile make us skip the fork
point. This scenario is inspired by the same happening in the
`transformer` repo, where a single universal resolution leads to a more
desirable lock without duplicate versions.

Consider a fresh run without preferences:
* We start with cleaver 2
* We fork
* We reject cleaver 2
* We find a cleaver 1 solution in fork 1 with foo 2 with bar 1
* We find a cleaver 1 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 between first and second run
unstable.

Design sketch:
```text
root -> cleaver, 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 diverging 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
```

Let's try this out:

```toml
[project]
name = "dummy"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
  "preferences-dependent-forking-5c5f76e6"
]

[tool.uv]
index-url = "http://127.0.0.1:3141/"
```

Cleanup and first, fresh run without preferences, on linux:
```console
$ rm -f uv.lock
$ uv venv -q
$ cargo run -q -- sync --preview
Resolved 6 packages in 647ms
Built dummy @ file:///home/konsti/projects/uv/debug/dummy
Built preferences-dependent-forking-5c5f76e6==0.0.0
Prepared 2 packages in 457ms
Installed 5 packages in 1ms
+ dummy==0.1.0 (from file:///home/konsti/projects/uv/debug/dummy)
+ preferences-dependent-forking-5c5f76e6==0.0.0
+ preferences-dependent-forking-bar-5c5f76e6==1.0.0
+ preferences-dependent-forking-cleaver-5c5f76e6==1.0.0
+ preferences-dependent-forking-foo-5c5f76e6==2.0.0
```

Second run, using the preferences from the lockfile:
```console
$ cargo run -q -- sync --preview
  Resolved 5 packages in 113ms
  Uninstalled 1 package in 0.57ms
  Installed 1 package in 1ms
- preferences-dependent-forking-foo-5c5f76e6==2.0.0
+ preferences-dependent-forking-foo-5c5f76e6==1.0.0
```

The logs confirm that in the first run with fork, while in the second we
don't.

In the second scenario `preferences-dependent-forking-conflicting`, we
add `foo 1 -> bar==2` so that the resolving cleaver 1 fails without
forking:

```toml
[project]
name = "dummy"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
  "preferences-dependent-forking-conflicting-5f0792c1"
]

[tool.uv]
index-url = "http://127.0.0.1:3141/"
```

Cleanup and first, fresh run, on linux:
```console
$ rm -f uv.lock
$ uv venv -q
$ cargo run -q -- sync --preview
Resolved 7 packages in 166ms
   Built dummy @ file:///home/konsti/projects/uv/debug/dummy
Prepared 1 package in 489ms
Installed 5 packages in 1ms
 + dummy==0.1.0 (from file:///home/konsti/projects/uv/debug/dummy)
 + preferences-dependent-forking-conflicting-5f0792c1==0.0.0
 + preferences-dependent-forking-conflicting-bar-5f0792c1==2.0.0
 + preferences-dependent-forking-conflicting-cleaver-5f0792c1==1.0.0
 + preferences-dependent-forking-conflicting-foo-5f0792c1==2.0.0
```

In the second run, we're using the preferences from the lockfile. We
reject preference cleaver 1 in universal mode, then move to cleaver 2,
fork, and fail in one of the forks by the cleaver 2 rejecting package:
```console
$ cargo run -q -- sync --preview
  × No solution found when resolving dependencies for split (sys_platform != 'linux'):
  ╰─▶ Because preferences-dependent-forking-conflicting-foo-5f0792c1==1.0.0
      depends on preferences-dependent-forking-conflicting-bar-5f0792c1==2 and
      preferences-dependent-forking-conflicting-cleaver-5f0792c1==1.0.0 depends on
      preferences-dependent-forking-conflicting-foo-5f0792c1{sys_platform == 'linux'}==1, we can
      conclude that preferences-dependent-forking-conflicting-cleaver-5f0792c1==1.0.0 depends on
      preferences-dependent-forking-conflicting-bar-5f0792c1==2.
      And because preferences-dependent-forking-conflicting-cleaver-5f0792c1==1.0.0 depends on
      preferences-dependent-forking-conflicting-bar-5f0792c1{sys_platform != 'linux'}==1 and only the following
      versions of preferences-dependent-forking-conflicting-cleaver-5f0792c1 are available:
          preferences-dependent-forking-conflicting-cleaver-5f0792c1==1.0.0
          preferences-dependent-forking-conflicting-cleaver-5f0792c1==2.0.0
      we can conclude that preferences-dependent-forking-conflicting-cleaver-5f0792c1<2.0.0 cannot be used. (1)

      Because preferences-dependent-forking-conflicting-reject-cleaver-2-5f0792c1==1.0.0
      depends on preferences-dependent-forking-conflicting-unrelated-dep-5f0792c1==3 and only
      preferences-dependent-forking-conflicting-reject-cleaver-2-5f0792c1==1.0.0 is available, we can
      conclude that all versions of preferences-dependent-forking-conflicting-reject-cleaver-2-5f0792c1 and
      preferences-dependent-forking-conflicting-unrelated-dep-5f0792c1{sys_platform != 'linux'}==2.0.0 are
      incompatible.
      And because preferences-dependent-forking-conflicting-cleaver-5f0792c1==2.0.0 depends on
      preferences-dependent-forking-conflicting-unrelated-dep-5f0792c1{sys_platform != 'linux'}==2
      and preferences-dependent-forking-conflicting-reject-cleaver-2-5f0792c1, we can conclude that
      preferences-dependent-forking-conflicting-cleaver-5f0792c1==2.0.0 cannot be used.
      And because we know from (1) that preferences-dependent-forking-conflicting-cleaver-5f0792c1<2.0.0 cannot
      be used, we can conclude that all versions of preferences-dependent-forking-conflicting-cleaver-5f0792c1
      cannot be used.
      And because preferences-dependent-forking-conflicting-5f0792c1==0.0.0 depends on
      preferences-dependent-forking-conflicting-cleaver-5f0792c1, we can conclude that
      preferences-dependent-forking-conflicting-5f0792c1==0.0.0 cannot be used.
      And because only preferences-dependent-forking-conflicting-5f0792c1==0.0.0 is available and dummy==0.1.0
      depends on preferences-dependent-forking-conflicting-5f0792c1, we can conclude that dummy==0.1.0 cannot
      be used.
      And because only dummy==0.1.0 is available and you require dummy, we can conclude that the requirements
      are unsatisfiable.
```

The logs confirm that we fork in both runs.

I don't know whether this scenario should be tagged `satisfiable` yet or
not.
  • Loading branch information
konstin committed Jul 25, 2024
1 parent 99f85df commit 85f5987
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 9 deletions.
79 changes: 79 additions & 0 deletions scenarios/fork/preferences-dependent-forking-conflicting.toml
Original file line number Diff line number Diff line change
@@ -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"]
76 changes: 76 additions & 0 deletions scenarios/fork/preferences-dependent-forking.toml
Original file line number Diff line number Diff line change
@@ -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"]
9 changes: 0 additions & 9 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 85f5987

Please sign in to comment.