Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/fromager/bootstrapper/_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,25 @@ def bootstrap(self, requirements: list[Requirement]) -> None:
Args:
requirements: Top-level requirements to resolve and bootstrap.
"""
# Warm the resolver cache with every top-level rule before adding any
# of them to the graph: a later rule for the same package (e.g. an
# exact pin that bypasses the release-age cooldown) changes what an
# earlier rule resolves to. Errors are reported by
# _resolve_and_add_top_level below.
for req in requirements:
Comment thread
rd4398 marked this conversation as resolved.
with req_ctxvar_context(req):
try:
self.resolve_versions(
req=req,
req_type=RequirementType.TOP_LEVEL,
parent_req=None,
return_all_versions=self.multiple_versions,
)
except Exception as err:
logger.debug(
"deferring resolution error to top-level handling: %s", err
)

# Resolve all top-level reqs and build initial stack.
# Use the token pattern (no try/finally) so that if resolution raises
# in normal mode, the context var stays set for the top-level error
Expand Down
80 changes: 80 additions & 0 deletions tests/test_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
)
from fromager.bootstrapper._work_item import WorkItem
from fromager.context import WorkContext
from fromager.dependency_graph import ROOT
from fromager.requirements_file import RequirementType, SourceType


Expand Down Expand Up @@ -955,6 +956,85 @@ def capture_run(self: Resolve, bt_arg: bootstrapper.Bootstrapper) -> list[Phase]
assert sorted(dispatch_calls) == ["pkg1", "pkg2"]


def _bootstrap_and_record_started(
bt: bootstrapper.Bootstrapper,
requirements: list[Requirement],
versions_by_rule: dict[str, list[tuple[str, Version]]],
) -> set[str]:
"""Run bootstrap() against a fake index and return the node keys Start saw.

versions_by_rule maps each top-level rule string to what the index
returns for it, which is how the release-age cooldown looks to the
bootstrapper: an unpinned rule does not see a version inside the
cooldown window, an exact pin does.
"""
started: set[str] = set()

def fake_lookup(
provider: typing.Any, req: Requirement, **kwargs: typing.Any
) -> list[tuple[str, Version]]:
return list(versions_by_rule[str(req)])

def fake_start_run(self: Start, bt_arg: bootstrapper.Bootstrapper) -> list[Phase]:
wi = self.work_item
started.add(f"{canonicalize_name(wi.req.name)}=={wi.resolved_version}")
return []

with (
patch.object(bt._resolver, "_resolve_from_graph", return_value=None),
patch("fromager.bootstrap_requirement_resolver.sources.get_source_provider"),
patch(
"fromager.bootstrap_requirement_resolver.resolver"
".find_all_matching_from_provider",
side_effect=fake_lookup,
),
patch.object(Start, "run", fake_start_run),
patch.object(bt, "_record_stack_state"),
):
bt.bootstrap(requirements)

return started


_OLD = ("https://files.test/pkg-0.39.0.tar.gz", Version("0.39.0"))
_NEW = ("https://files.test/pkg-0.39.1.tar.gz", Version("0.39.1"))


def test_bootstrap_toplevel_unpinned_then_pin_leaves_no_stale_node(
tmp_context: WorkContext,
) -> None:
bt = bootstrapper.Bootstrapper(tmp_context)
requirements = [Requirement("pkg"), Requirement("pkg==0.39.1")]

started = _bootstrap_and_record_started(
bt, requirements, {"pkg": [_OLD], "pkg==0.39.1": [_NEW]}
)

graph_nodes = set(tmp_context.dependency_graph.nodes) - {ROOT}
assert started == {"pkg==0.39.1"}
assert graph_nodes == started
root_edges = tmp_context.dependency_graph.nodes[ROOT].children
assert sorted(str(edge.req) for edge in root_edges) == ["pkg", "pkg==0.39.1"]
assert {edge.destination_node.key for edge in root_edges} == {"pkg==0.39.1"}


def test_bootstrap_toplevel_multiple_versions_graph_matches_started(
tmp_context: WorkContext,
) -> None:
bt = bootstrapper.Bootstrapper(tmp_context, multiple_versions=True)
requirements = [Requirement("pkg"), Requirement("pkg==0.39.1")]

with patch("fromager.bootstrapper._resolve._cache.find_cached_wheel") as cached:
cached.return_value = (None, None)
started = _bootstrap_and_record_started(
bt, requirements, {"pkg": [_OLD], "pkg==0.39.1": [_NEW]}
)

graph_nodes = set(tmp_context.dependency_graph.nodes) - {ROOT}
assert started == {"pkg==0.39.0", "pkg==0.39.1"}
assert graph_nodes == started


def test_bg_prepare_source_log_prefix_includes_version(
tmp_context: WorkContext,
caplog: pytest.LogCaptureFixture,
Expand Down
Loading