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
3 changes: 3 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ jobs:
- name: Check the index against the pins
run: python tools/pins.py verify build/index

- name: Check no package name is also micropython-lib's
run: python tools/pins.py overlap build/index

- name: Prepare the Pages artifact
run: touch build/index/.nojekyll

Expand Down
39 changes: 39 additions & 0 deletions tests/test_pins.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import sys
import tempfile
import unittest
from contextlib import redirect_stdout
from io import StringIO
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
Expand Down Expand Up @@ -210,3 +212,40 @@ def test_a_root_that_names_another_repository_is_refused(self):
{"repository": "PyDevices/audioinstruments", "ref": "v0.5.0", "formerly": ["PyDevices/audiocomponents"]},
{"repository": "PyDevices/somewhere-else", "ref": "v0.4.0"},
)


class Overlap(unittest.TestCase):
"""overlap compares a built index with an upstream one, here a folder served from this machine."""

def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.addCleanup(self._tmp.cleanup)
self.dir = Path(self._tmp.name)
self.index = self.dir / "index"
(self.index / "untested").mkdir(parents=True)
(self.index / "index.json").write_text(json.dumps({"packages": [{"name": "bledev"}]}))
(self.index / "untested" / "index.json").write_text(json.dumps({"packages": [{"name": "bledev"}, {"name": "gt911"}]}))
self.upstream = self.dir / "upstream"
self.upstream.mkdir()

def theirs(self, *names):
(self.upstream / "index.json").write_text(json.dumps({"packages": [{"name": n} for n in names]}))
return self.upstream.as_uri()

def test_no_shared_name_passes(self):
with redirect_stdout(StringIO()) as out:
pins.overlap(self.index, self.theirs("aioble", "sdcard"))
self.assertIn("none shared", out.getvalue())

def test_a_shared_name_fails_and_names_it(self):
with self.assertRaisesRegex(SystemExit, "bledev"):
pins.overlap(self.index, self.theirs("aioble", "bledev"))

def test_a_name_only_on_the_untested_index_counts(self):
with self.assertRaisesRegex(SystemExit, "gt911"):
pins.overlap(self.index, self.theirs("gt911"))

def test_an_unreadable_upstream_warns_and_passes(self):
with redirect_stdout(StringIO()) as out:
pins.overlap(self.index, (self.dir / "not-there").as_uri())
self.assertIn("::warning::", out.getvalue())
43 changes: 41 additions & 2 deletions tools/pins.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@
GitHub later replaces in the queue is still in ``pins.json`` for the build
that replaces it.
``fetch`` checks the pinned sources out, ``lock`` writes the pinned tags into
the checkout's ``lib-lock.json`` for its builder, and ``verify`` checks a built
index has every package at the version pinned.
the checkout's ``lib-lock.json`` for its builder, ``verify`` checks a built
index has every package at the version pinned, and ``overlap`` checks that none
of its names is also one of micropython-lib's.

Standard library and git only.
"""
Expand All @@ -43,10 +44,14 @@
import subprocess
import sys
import time
import urllib.request
from pathlib import Path

PINS = Path(__file__).resolve().parent.parent / "pins.json"

#: micropython-lib's index, whose names ours must not share.
UPSTREAM_INDEX = "https://micropython.org/pi/v2"

#: Profiles whose tag names a folder of the library repository.
FOLDER_PROFILES = ("palettes", "pdwidgets")

Expand Down Expand Up @@ -227,6 +232,35 @@ def verify(index: Path) -> None:
print("the index matches pins.json")


def package_names(index: Path) -> set[str]:
"""Every package name a built index publishes, its untested index included."""
names: set[str] = set()
for path in (index / "index.json", index / "untested" / "index.json"):
if path.is_file():
names.update(p["name"] for p in json.loads(path.read_text(encoding="utf-8"))["packages"])
return names


def overlap(index: Path, upstream: str = UPSTREAM_INDEX) -> None:
"""No package here is named as one of micropython-lib's.

omip looks here first, so a shared name would hide theirs. Failing to read
their index only warns: their outage shouldn't hold up a release of ours.
"""
url = f"{upstream.rstrip('/')}/index.json"
try:
with urllib.request.urlopen(url, timeout=30) as response:
theirs = {p["name"] for p in json.loads(response.read())["packages"]}
except (OSError, ValueError, KeyError) as exc:
print(f"::warning::can't read {url}, so names weren't compared with micropython-lib's: {exc}")
return
ours = package_names(index)
shared = sorted(ours & theirs)
if shared:
raise SystemExit(f"these package names are micropython-lib's too, and omip would hide theirs: {', '.join(shared)}")
print(f"{len(ours)} package names here, {len(theirs)} in micropython-lib, none shared")


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
commands = parser.add_subparsers(dest="command", required=True)
Expand All @@ -245,6 +279,9 @@ def main() -> None:
k.add_argument("root", type=Path)
v = commands.add_parser("verify", help="check a built index against pins.json")
v.add_argument("index", type=Path)
o = commands.add_parser("overlap", help="check a built index's names against micropython-lib's")
o.add_argument("index", type=Path)
o.add_argument("--upstream", default=UPSTREAM_INDEX)
args = parser.parse_args()
if args.command == "apply":
apply(args.profile, args.repository, args.ref)
Expand All @@ -254,6 +291,8 @@ def main() -> None:
fetch(args.out, args.root_ref)
elif args.command == "lock":
lock(args.root)
elif args.command == "overlap":
overlap(args.index, args.upstream)
else:
verify(args.index)

Expand Down
Loading