Skip to content
Closed
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
35 changes: 24 additions & 11 deletions scripts/ci/install_base_python_locks.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@
re.IGNORECASE,
),
re.compile(r"requires a different Python", re.IGNORECASE),
# A base lock can pin a version that has since been yanked or that offers no
# wheel for the pinned coverage-image interpreter. pip proves the index was
# reachable only when it lists at least one concrete version. Empty lists,
# ``none``, and unreachable-index diagnostics remain fatal.
re.compile(
r"Could not find a version that satisfies the requirement[^\n]*"
r"\(from versions:\s*(?!none\b)(?=[A-Za-z0-9])[^)\n]+\)",
re.IGNORECASE,
),
)
Runner = Callable[..., subprocess.CompletedProcess[str]]

Expand Down Expand Up @@ -150,12 +159,16 @@ def _is_deferable_preflight_failure(output: str) -> bool:
"""Return whether a failed candidate may be grouped or safely skipped.

A hash-bearing supplement can fail pip's independent-closure check because a
transitive pin/hash lives in a sibling lock, and a base lock can explicitly
reject the pinned coverage-image interpreter. Those states are safe to
recover through a same-directory group or defer to the later networkless
coverage run. Hash mismatches, resolver crashes, empty diagnostics, and
registry/network failures remain fatal so a broken trusted build cannot be
mistaken for an optional lock.
transitive pin/hash lives in a sibling lock, a base lock can explicitly
reject the pinned coverage-image interpreter, and a base lock can pin a
version the reachable index no longer offers for that interpreter (yanked or
no matching wheel). Those states are safe to recover through a same-directory
group or defer to the later networkless coverage run. Hash mismatches,
resolver crashes, empty diagnostics, and registry/network failures — including
empty or ``none`` version lists — remain fatal so a broken trusted build cannot
be mistaken for an optional lock. Deferred paths retain a warning and bounded
pip diagnostics so the incompatibility stays visible without blocking
unrelated coverage evidence.
"""
return bool(output.strip()) and any(
pattern.search(output) for pattern in DEFERABLE_PREFLIGHT_FAILURES
Expand All @@ -171,8 +184,9 @@ def _report_fatal_preflight_failure(
"""Publish one bounded, source-aware fatal preflight failure."""
print(
"::error::Trusted base Python lock preflight failed for "
f"{entry_label}; only incomplete hash closures or explicit Python "
"interpreter incompatibility may be deferred.",
f"{entry_label}; only incomplete hash closures, explicit Python "
"interpreter incompatibility, or a reachable-index version that is no "
"longer available for the coverage interpreter may be deferred.",
file=stderr,
)
failure_output = _bounded_failure_output(output)
Expand Down Expand Up @@ -280,9 +294,8 @@ def install_materialized_locks(
skipped += 1
print(
"::warning::Skipping trusted base Python requirement candidate "
f"{entry.source}: hash-bearing content is not an independently "
"installable dependency closure and no same-directory lock group "
"completed it.",
f"{entry.source}: it could not be installed independently for the "
"coverage interpreter and no same-directory lock group completed it.",
file=stderr,
)
failure_output = _bounded_failure_output(
Expand Down
97 changes: 97 additions & 0 deletions tests/test_install_base_python_lock_missing_pin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""Regression tests for unavailable pins in trusted base Python locks."""

from __future__ import annotations

import io
import json
import subprocess
from pathlib import Path

from scripts.ci import install_base_python_locks as installer


def _write_candidate(root: Path) -> None:
"""Write one trusted materialized lock candidate and its manifest."""

(root / "manifest.json").write_text(
json.dumps(
[
{
"file": "requirements-000.txt",
"source": "requirements-hashes.txt",
}
]
),
encoding="utf-8",
)
(root / "requirements-000.txt").write_text(
"pypdf==6.13.3 --hash=sha256:" + ("a" * 64) + "\n",
encoding="utf-8",
)


def _run_preflight_failure(root: Path, output: str) -> tuple[int, str, str]:
"""Run the installer with one deterministic pip preflight failure."""

_write_candidate(root)

def fake_runner(command: list[str], **kwargs):
return subprocess.CompletedProcess(command, 1, stdout=output)

stdout = io.StringIO()
stderr = io.StringIO()
result = installer.install_materialized_locks(
root,
runner=fake_runner,
stdout=stdout,
stderr=stderr,
)
return result, stdout.getvalue(), stderr.getvalue()


def test_reachable_index_missing_pin_is_visible_and_nonfatal(tmp_path: Path) -> None:
"""A reachable index proving newer versions exist may defer a stale pin."""

output = (
"ERROR: Could not find a version that satisfies the requirement "
"pypdf==6.13.3 (from versions: 6.14.1, 6.14.2)\n"
"ERROR: No matching distribution found for pypdf==6.13.3"
)

result, stdout, stderr = _run_preflight_failure(tmp_path, output)

assert result == 0
assert "candidates=1 installed=0 skipped=1" in stdout
assert "Could not find a version that satisfies the requirement" in stderr


def test_empty_index_missing_pin_remains_fatal(tmp_path: Path) -> None:
"""An explicitly empty package index must never be treated as optional."""

output = (
"ERROR: Could not find a version that satisfies the requirement "
"pypdf==6.13.3 (from versions: none)\n"
"ERROR: No matching distribution found for pypdf==6.13.3"
)

result, stdout, stderr = _run_preflight_failure(tmp_path, output)

assert result == 1
assert "preflight failed" in stderr
assert "installed=" not in stdout


def test_blank_version_list_missing_pin_remains_fatal(tmp_path: Path) -> None:
"""A blank version list is not affirmative proof that the index is reachable."""

output = (
"ERROR: Could not find a version that satisfies the requirement "
"pypdf==6.13.3 (from versions: )\n"
"ERROR: No matching distribution found for pypdf==6.13.3"
)

result, stdout, stderr = _run_preflight_failure(tmp_path, output)

assert result == 1
assert "preflight failed" in stderr
assert "installed=" not in stdout
Loading