uv/scripts/scenarios/update.py
Zanie Blue f3ef55f879
Update the scenarios to use vendored build dependencies (#1605)
Uses `--find-links` to discover vendored scenario build dependencies and
allows us to use `--index-url` instead of `--extra-index-url` to avoid
hitting the real PyPI in scenario tests.
2024-02-19 21:55:03 +00:00

251 lines
6.7 KiB
Python
Executable file

#!/usr/bin/env python3
"""
Generates and updates snapshot test cases from packse scenarios.
Usage:
Regenerate the scenario test file:
$ ./scripts/scenarios/update.py
Scenarios are pinned to a specific commit. Change the `PACKSE_COMMIT` constant to update them.
Scenarios can be developed locally with the following workflow:
Install the local version of packse
$ pip install -e <path to packse>
From the packse repository, build and publish the scenarios to a local index
$ packse index up --bg
$ packse build scenarios/*
$ packse publish dist/* --index-url http://localhost:3141/packages/local --anonymous
Override the default PyPI index for uv and update the scenarios
$ UV_INDEX_URL="http://localhost:3141/packages/all/+simple" ./scripts/scenarios/update.py
Requirements:
Requires `packse` and `chevron-blue`.
$ pip install -r scripts/scenarios/requirements.txt
Also supports a local, editable requirement on `packse`.
Uses `git`, `rustfmt`, and `cargo insta test` requirements from the project.
"""
import json
import shutil
import subprocess
import sys
import textwrap
from pathlib import Path
PACKSE_COMMIT = "de0bab473eeaa4445db5a8febd732c655fad3d52"
TOOL_ROOT = Path(__file__).parent
TEMPLATES = TOOL_ROOT / "templates"
INSTALL_TEMPLATE = TEMPLATES / "install.mustache"
COMPILE_TEMPLATE = TEMPLATES / "compile.mustache"
PACKSE = TOOL_ROOT / "packse-scenarios"
REQUIREMENTS = TOOL_ROOT / "requirements.txt"
PROJECT_ROOT = TOOL_ROOT.parent.parent
TESTS = PROJECT_ROOT / "crates" / "uv" / "tests"
INSTALL_TESTS = TESTS / "pip_install_scenarios.rs"
COMPILE_TESTS = TESTS / "pip_compile_scenarios.rs"
CUTE_NAMES = {
"a": "albatross",
"b": "bluebird",
"c": "crow",
"d": "duck",
"e": "eagle",
"f": "flamingo",
"g": "goose",
"h": "heron",
}
try:
import packse
except ImportError:
print(
f"missing requirement `packse`: install the requirements at {REQUIREMENTS.relative_to(PROJECT_ROOT)}",
file=sys.stderr,
)
exit(1)
try:
import chevron_blue
except ImportError:
print(
f"missing requirement `chevron-blue`: install the requirements at {REQUIREMENTS.relative_to(PROJECT_ROOT)}",
file=sys.stderr,
)
exit(1)
if packse.__development_base_path__.name != "packse":
# Not a local editable installation, download latest scenarios
if PACKSE.exists():
shutil.rmtree(PACKSE)
print("Downloading scenarios from packse repository...", file=sys.stderr)
# Perform a sparse checkout where we only grab the `scenarios` folder
subprocess.check_call(
[
"git",
"clone",
"-n",
"--depth=1",
"--filter=tree:0",
"https://github.com/zanieb/packse",
str(PACKSE),
],
cwd=TOOL_ROOT,
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
)
subprocess.check_call(
["git", "sparse-checkout", "set", "--no-cone", "scenarios"],
cwd=PACKSE,
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
)
subprocess.check_call(
["git", "checkout", PACKSE_COMMIT],
cwd=PACKSE,
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
)
scenarios_path = str(PACKSE / "scenarios")
commit = PACKSE_COMMIT
else:
print(
f"Using scenarios in packse repository at {packse.__development_base_path__}",
file=sys.stderr,
)
scenarios_path = str(packse.__development_base_path__ / "scenarios")
# Get the commit from the repository
commit = (
subprocess.check_output(
["git", "show", "-s", "--format=%H", "HEAD"],
cwd=packse.__development_base_path__,
)
.decode()
.strip()
)
if commit != PACKSE_COMMIT:
print(f"WARNING: Expected commit {PACKSE_COMMIT!r} but found {commit!r}.")
print("Loading scenario metadata...", file=sys.stderr)
data = json.loads(
subprocess.check_output(
[
sys.executable,
"-m",
"packse",
"inspect",
"--short-names",
scenarios_path,
],
)
)
data["scenarios"] = [
scenario
for scenario in data["scenarios"]
# Drop the example scenario
if scenario["name"] != "example"
]
# Wrap the description onto multiple lines
for scenario in data["scenarios"]:
scenario["description_lines"] = textwrap.wrap(scenario["description"], width=80)
# Wrap the expected explanation onto multiple lines
for scenario in data["scenarios"]:
expected = scenario["expected"]
expected["explanation_lines"] = (
textwrap.wrap(expected["explanation"], width=80)
if expected["explanation"]
else []
)
# Generate cute names for each scenario
for scenario in data["scenarios"]:
for package in scenario["packages"]:
package["cute_name"] = CUTE_NAMES[package["name"][0]]
# Split scenarios into `install` and `compile` cases
install_scenarios = []
compile_scenarios = []
for scenario in data["scenarios"]:
if (scenario["resolver_options"] or {}).get("python") is not None:
compile_scenarios.append(scenario)
else:
install_scenarios.append(scenario)
for template, tests, scenarios in [
(INSTALL_TEMPLATE, INSTALL_TESTS, install_scenarios),
(COMPILE_TEMPLATE, COMPILE_TESTS, compile_scenarios),
]:
data = {"scenarios": scenarios}
# Add generated metadata
data["generated_from"] = f"https://github.com/zanieb/packse/tree/{commit}/scenarios"
data["generated_with"] = " ".join(sys.argv)
data[
"vendor_links"
] = f"https://raw.githubusercontent.com/zanieb/packse/{commit}/vendor/links.html"
# Render the template
print(f"Rendering template {template.name}", file=sys.stderr)
output = chevron_blue.render(
template=template.read_text(), data=data, no_escape=True, warn=True
)
# Update the test files
print(
f"Updating test file at `{tests.relative_to(PROJECT_ROOT)}`...",
file=sys.stderr,
)
with open(tests, "wt") as test_file:
test_file.write(output)
# Format
print(
"Formatting test file...",
file=sys.stderr,
)
subprocess.check_call(["rustfmt", str(tests)])
# Update snapshots
print("Updating snapshots...\n", file=sys.stderr)
subprocess.call(
[
"cargo",
"insta",
"test",
"--features",
"pypi,python",
"--accept",
"--test-runner",
"nextest",
"--test",
tests.with_suffix("").name,
],
cwd=PROJECT_ROOT,
)
print("\nDone!", file=sys.stderr)