Skip to content

Commit e85c741

Browse files
committed
lots of small test improvements and speedups
1 parent 18eeb66 commit e85c741

11 files changed

Lines changed: 483 additions & 382 deletions

.github/workflows/tests.yml

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -113,29 +113,34 @@ jobs:
113113
- name: Setup pnpm
114114
uses: pnpm/action-setup@v5.0.0
115115
with:
116-
version: 10
116+
version: 10.19.0
117117

118118
- name: Setup Node
119119
uses: actions/setup-node@v6
120120
with:
121121
node-version: '22'
122122

123-
- name: Setup Yarn (Berry / 4.x via corepack)
123+
- name: Setup Yarn (classic + Berry)
124124
run: |
125-
# github-hosted ubuntu runners ship a system-wide yarn 1.22 that
126-
# shadows corepack's shim. Wipe it out before enabling corepack so
127-
# `yarn` unambiguously resolves to the 4.x shim from node's bin dir.
128-
sudo rm -f /usr/local/bin/yarn /usr/local/bin/yarnpkg /usr/bin/yarn /usr/bin/yarnpkg
129-
corepack enable
130-
corepack prepare yarn@4.13.0 --activate
131-
# Pre-download pnpm via corepack so the very first ``pnpm --version``
132-
# doesn't pollute logs with "Corepack is about to download ..."
133-
# progress output.
134-
corepack prepare pnpm@10.17.1 --activate || true
125+
npm install -g yarn@1.22.22
126+
if [ "$(uname -s)" = "Darwin" ]; then
127+
YARN_BERRY_PREFIX="/opt/homebrew/opt/yarn-berry"
128+
YARN_BERRY_ALIAS="/opt/homebrew/bin/yarn-berry"
129+
elif [ -d /home/linuxbrew/.linuxbrew/opt ]; then
130+
YARN_BERRY_PREFIX="/home/linuxbrew/.linuxbrew/opt/yarn-berry"
131+
YARN_BERRY_ALIAS="/home/linuxbrew/.linuxbrew/bin/yarn-berry"
132+
else
133+
YARN_BERRY_PREFIX="/usr/local/yarn-berry"
134+
YARN_BERRY_ALIAS="/usr/local/bin/yarn-berry"
135+
fi
136+
npm install --prefix "$YARN_BERRY_PREFIX" @yarnpkg/cli-dist@4.13.0
137+
ln -sf "$YARN_BERRY_PREFIX/node_modules/.bin/yarn" "$YARN_BERRY_ALIAS"
138+
"$YARN_BERRY_ALIAS" --version | grep -q '^4\.'
135139
which yarn
136140
yarn --version
137-
yarn --version | grep -q '^4\.' || { echo "ERROR: yarn is not 4.x"; exit 1; }
138-
pnpm --version || true
141+
which yarn-berry
142+
yarn-berry --version
143+
yarn --version | grep -q '^1\.' || { echo "ERROR: yarn is not 1.x"; exit 1; }
139144
140145
- name: Setup Bun
141146
uses: oven-sh/setup-bun@v2
@@ -278,29 +283,34 @@ jobs:
278283
- name: Setup pnpm
279284
uses: pnpm/action-setup@v5.0.0
280285
with:
281-
version: 10
286+
version: 10.19.0
282287

283288
- name: Setup Node
284289
uses: actions/setup-node@v6
285290
with:
286291
node-version: '22'
287292

288-
- name: Setup Yarn (Berry / 4.x via corepack)
293+
- name: Setup Yarn (classic + Berry)
289294
run: |
290-
# github-hosted ubuntu runners ship a system-wide yarn 1.22 that
291-
# shadows corepack's shim. Wipe it out before enabling corepack so
292-
# `yarn` unambiguously resolves to the 4.x shim from node's bin dir.
293-
sudo rm -f /usr/local/bin/yarn /usr/local/bin/yarnpkg /usr/bin/yarn /usr/bin/yarnpkg
294-
corepack enable
295-
corepack prepare yarn@4.13.0 --activate
296-
# Pre-download pnpm via corepack so the very first ``pnpm --version``
297-
# doesn't pollute logs with "Corepack is about to download ..."
298-
# progress output.
299-
corepack prepare pnpm@10.17.1 --activate || true
295+
npm install -g yarn@1.22.22
296+
if [ "$(uname -s)" = "Darwin" ]; then
297+
YARN_BERRY_PREFIX="/opt/homebrew/opt/yarn-berry"
298+
YARN_BERRY_ALIAS="/opt/homebrew/bin/yarn-berry"
299+
elif [ -d /home/linuxbrew/.linuxbrew/opt ]; then
300+
YARN_BERRY_PREFIX="/home/linuxbrew/.linuxbrew/opt/yarn-berry"
301+
YARN_BERRY_ALIAS="/home/linuxbrew/.linuxbrew/bin/yarn-berry"
302+
else
303+
YARN_BERRY_PREFIX="/usr/local/yarn-berry"
304+
YARN_BERRY_ALIAS="/usr/local/bin/yarn-berry"
305+
fi
306+
npm install --prefix "$YARN_BERRY_PREFIX" @yarnpkg/cli-dist@4.13.0
307+
ln -sf "$YARN_BERRY_PREFIX/node_modules/.bin/yarn" "$YARN_BERRY_ALIAS"
308+
"$YARN_BERRY_ALIAS" --version | grep -q '^4\.'
300309
which yarn
301310
yarn --version
302-
yarn --version | grep -q '^4\.' || { echo "ERROR: yarn is not 4.x"; exit 1; }
303-
pnpm --version || true
311+
which yarn-berry
312+
yarn-berry --version
313+
yarn --version | grep -q '^1\.' || { echo "ERROR: yarn is not 1.x"; exit 1; }
304314
305315
- name: Setup Bun
306316
uses: oven-sh/setup-bun@v2

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -855,12 +855,12 @@ Source: [`abx_pkg/binprovider_yarn.py`](./abx_pkg/binprovider_yarn.py) • Tests
855855
```python
856856
INSTALLER_BIN = "yarn"
857857
PATH = "" # prepends <yarn_prefix>/node_modules/.bin
858-
yarn_prefix = None # workspace dir, defaults to ABX_PKG_YARN_ROOT or ~/.cache/abx-pkg/yarn
858+
yarn_prefix = None # workspace dir, defaults to ABX_PKG_YARN_ROOT or ABX_PKG_LIB_DIR/yarn
859859
cache_dir = user_cache_path("yarn", "abx-pkg") or <system temp>/yarn-cache
860860
yarn_install_args = []
861861
```
862862

863-
- Install root: Yarn 4 / Yarn Berry is workspace-based, so the provider always operates inside a project directory. Set `install_root=Path(...)` or `install_root=Path(...)` for a hermetic workspace; the workspace is auto-initialized with a stub `package.json` and `.yarnrc.yml` (`nodeLinker: node-modules` so binaries land in `<workspace>/node_modules/.bin`). When unset, the provider uses `$ABX_PKG_YARN_ROOT` or `~/.cache/abx-pkg/yarn`.
863+
- Install root: Yarn 4 / Yarn Berry is workspace-based, so the provider always operates inside a project directory. Set `install_root=Path(...)` or `install_root=Path(...)` for a hermetic workspace; the workspace is auto-initialized with a stub `package.json` and `.yarnrc.yml` (`nodeLinker: node-modules` so binaries land in `<workspace>/node_modules/.bin`). When unset, the provider uses `$ABX_PKG_YARN_ROOT` or `$ABX_PKG_LIB_DIR/yarn`.
864864
- Auto-switching: none. Honors `YARN_BINARY=/abs/path/to/yarn`. Both Yarn classic (1.x) and Yarn Berry (2+) work for basic install/update/uninstall, but only Yarn 4.10+ supports the security flags.
865865
- `dry_run`: shared behavior.
866866
- Security: supports both `min_release_age` and `postinstall_scripts=False`, and hydrates their provider defaults from `ABX_PKG_MIN_RELEASE_AGE` and `ABX_PKG_POSTINSTALL_SCRIPTS`. Both controls require Yarn 4.10+; on older hosts `supports_min_release_age()` / `supports_postinstall_disable()` return `False` and explicit values are logged-and-ignored.
@@ -923,7 +923,7 @@ Source: [`abx_pkg/binprovider_bash.py`](./abx_pkg/binprovider_bash.py) • Tests
923923
```python
924924
INSTALLER_BIN = "sh"
925925
PATH = ""
926-
bash_root = $ABX_PKG_BASH_ROOT or ~/.cache/abx-pkg/bash
926+
bash_root = $ABX_PKG_BASH_ROOT or $ABX_PKG_LIB_DIR/bash
927927
bash_bin_dir = <bash_root>/bin
928928
```
929929

@@ -1035,7 +1035,7 @@ Source: [`abx_pkg/binprovider_docker.py`](./abx_pkg/binprovider_docker.py) • T
10351035
```python
10361036
INSTALLER_BIN = "docker"
10371037
PATH = "" # prepends docker_shim_dir
1038-
docker_shim_dir = ($ABX_PKG_DOCKER_ROOT or ~/.cache/abx-pkg/docker) / "bin"
1038+
docker_shim_dir = ($ABX_PKG_DOCKER_ROOT or $ABX_PKG_LIB_DIR/docker) / "bin"
10391039
docker_run_args = ["--rm", "-i"]
10401040
```
10411041

@@ -1056,7 +1056,7 @@ Source: [`abx_pkg/binprovider_chromewebstore.py`](./abx_pkg/binprovider_chromewe
10561056
```python
10571057
INSTALLER_BIN = "node"
10581058
PATH = ""
1059-
extensions_root = $ABX_PKG_CHROMEWEBSTORE_ROOT or ~/.cache/abx-pkg/chromewebstore
1059+
extensions_root = $ABX_PKG_CHROMEWEBSTORE_ROOT or $ABX_PKG_LIB_DIR/chromewebstore
10601060
extensions_dir = <extensions_root>/extensions
10611061
```
10621062

@@ -1077,7 +1077,7 @@ Source: [`abx_pkg/binprovider_puppeteer.py`](./abx_pkg/binprovider_puppeteer.py)
10771077
```python
10781078
INSTALLER_BIN = "puppeteer-browsers"
10791079
PATH = ""
1080-
puppeteer_root = $ABX_PKG_PUPPETEER_ROOT or ~/.cache/abx-pkg/puppeteer
1080+
puppeteer_root = $ABX_PKG_PUPPETEER_ROOT or $ABX_PKG_LIB_DIR/puppeteer
10811081
browser_bin_dir = <puppeteer_root>/bin
10821082
browser_cache_dir = <puppeteer_root>/cache
10831083
```

abx_pkg/binprovider.py

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import functools
1414
import tempfile
1515
from contextvars import ContextVar
16-
from types import SimpleNamespace
1716

1817
from typing import (
1918
Optional,
@@ -453,17 +452,22 @@ def detect_euid(
453452

454453
return candidate_euid if candidate_euid is not None else current_euid
455454

456-
def get_pw_record(self, uid: int) -> Any:
455+
def get_pw_record(self, uid: int) -> pwd.struct_passwd:
457456
try:
458457
return pwd.getpwuid(uid)
459458
except KeyError:
460459
if uid != os.geteuid():
461460
raise
462-
return SimpleNamespace(
463-
pw_uid=uid,
464-
pw_gid=os.getegid(),
465-
pw_dir=os.environ.get("HOME", tempfile.gettempdir()),
466-
pw_name=os.environ.get("USER") or os.environ.get("LOGNAME") or str(uid),
461+
return pwd.struct_passwd(
462+
(
463+
os.environ.get("USER") or os.environ.get("LOGNAME") or str(uid),
464+
"x",
465+
uid,
466+
os.getegid(),
467+
"",
468+
os.environ.get("HOME", tempfile.gettempdir()),
469+
os.environ.get("SHELL", "/bin/sh"),
470+
),
467471
)
468472

469473
@property
@@ -542,7 +546,7 @@ def INSTALLER_BINARY(self, no_cache: bool = False) -> ShallowBinary:
542546
self.INSTALLER_BIN,
543547
)
544548

545-
@computed_field
549+
@computed_field(repr=False)
546550
@property
547551
def is_valid(self) -> bool:
548552
try:
@@ -985,6 +989,7 @@ def exec(
985989
)
986990
cwd_path = Path(cwd).resolve()
987991
cmd = [str(bin_abspath), *(str(arg) for arg in cmd)]
992+
is_version_probe = len(cmd) == 2 and cmd[1] in {"--version", "-version", "-v"}
988993
exec_log_prefix = ACTIVE_EXEC_LOG_PREFIX.get()
989994
if should_log_command:
990995
if exec_log_prefix:
@@ -1033,7 +1038,7 @@ def drop_privileges():
10331038
except Exception:
10341039
pass
10351040

1036-
if self.dry_run:
1041+
if self.dry_run and not is_version_probe:
10371042
return subprocess.CompletedProcess(cmd, 0, "", "skipped (dry run)")
10381043

10391044
kwargs.setdefault("capture_output", True)
@@ -1404,15 +1409,14 @@ def install(
14041409
if self.dry_run:
14051410
# return fake ShallowBinary if we're just doing a dry run
14061411
# no point trying to get real abspath or version if nothing was actually installed
1407-
return ShallowBinary.model_validate(
1408-
{
1409-
"name": bin_name,
1410-
"binprovider": self,
1411-
"abspath": Path(shutil.which(bin_name) or UNKNOWN_ABSPATH),
1412-
"version": UNKNOWN_VERSION,
1413-
"sha256": UNKNOWN_SHA256,
1414-
"binproviders": [self],
1415-
},
1412+
return ShallowBinary.model_construct(
1413+
name=bin_name,
1414+
description=bin_name,
1415+
loaded_binprovider=self,
1416+
loaded_abspath=UNKNOWN_ABSPATH,
1417+
loaded_version=UNKNOWN_VERSION,
1418+
loaded_sha256=UNKNOWN_SHA256,
1419+
binproviders=[self],
14161420
)
14171421

14181422
self.invalidate_cache(bin_name)
@@ -1558,15 +1562,14 @@ def update(
15581562
ACTIVE_EXEC_LOG_PREFIX.reset(exec_log_prefix_token)
15591563

15601564
if self.dry_run:
1561-
return ShallowBinary.model_validate(
1562-
{
1563-
"name": bin_name,
1564-
"binprovider": self,
1565-
"abspath": Path(shutil.which(bin_name) or UNKNOWN_ABSPATH),
1566-
"version": UNKNOWN_VERSION,
1567-
"sha256": UNKNOWN_SHA256,
1568-
"binproviders": [self],
1569-
},
1565+
return ShallowBinary.model_construct(
1566+
name=bin_name,
1567+
description=bin_name,
1568+
loaded_binprovider=self,
1569+
loaded_abspath=UNKNOWN_ABSPATH,
1570+
loaded_version=UNKNOWN_VERSION,
1571+
loaded_sha256=UNKNOWN_SHA256,
1572+
binproviders=[self],
15701573
)
15711574

15721575
self.invalidate_cache(bin_name)

abx_pkg/binprovider_playwright.py

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -179,15 +179,41 @@ def setup(
179179
min_version: SemVer | None = None,
180180
no_cache: bool = False,
181181
) -> None:
182+
if self.install_root is not None:
183+
self.install_root.mkdir(parents=True, exist_ok=True)
184+
if self.bin_dir is not None:
185+
self.bin_dir.mkdir(parents=True, exist_ok=True)
186+
lib_dir = os.environ.get("ABX_PKG_LIB_DIR")
187+
if (
188+
self.install_root is not None
189+
and lib_dir
190+
and str(self.install_root).startswith(lib_dir.rstrip("/") + "/")
191+
):
192+
npm_install_root = Path(lib_dir) / "npm"
193+
elif self.install_root is not None:
194+
npm_install_root = self.install_root / "npm"
195+
else:
196+
npm_install_root = None
197+
expected_playwright_module = (
198+
npm_install_root / "node_modules" / "playwright"
199+
if npm_install_root is not None
200+
else None
201+
)
182202
try:
183203
cached = self.INSTALLER_BINARY(no_cache=no_cache)
184204
except Exception:
185205
cached = None
186-
if cached and cached.loaded_abspath:
206+
if (
207+
cached
208+
and cached.loaded_abspath
209+
and (
210+
expected_playwright_module is None
211+
or expected_playwright_module.is_dir()
212+
)
213+
):
187214
path_entries: list[Path] = []
188215
if self.bin_dir is not None:
189216
path_entries.append(self.bin_dir)
190-
lib_dir = os.environ.get("ABX_PKG_LIB_DIR")
191217
if self.install_root is not None and (
192218
not lib_dir
193219
or not str(self.install_root).startswith(lib_dir.rstrip("/") + "/")
@@ -202,11 +228,6 @@ def setup(
202228
prepend=True,
203229
)
204230
return
205-
if self.install_root is not None:
206-
self.install_root.mkdir(parents=True, exist_ok=True)
207-
if self.bin_dir is not None:
208-
self.bin_dir.mkdir(parents=True, exist_ok=True)
209-
210231
# Bootstrap the ``playwright`` npm package (which ships the CLI
211232
# and its ``playwright-core`` peer). Nest it under
212233
# ``playwright_root`` when one is pinned; otherwise leave
@@ -235,17 +256,12 @@ def setup(
235256
# Hermetic: install_root/npm
236257
# Managed LIB_DIR: LIB_DIR/npm (shared with NpmProvider)
237258
# Global: no install_root (NpmProvider picks its own default)
238-
lib_dir = os.environ.get("ABX_PKG_LIB_DIR")
239259
if (
240260
self.install_root is not None
241261
and lib_dir
242262
and str(self.install_root).startswith(lib_dir.rstrip("/") + "/")
243263
):
244264
npm_install_root = Path(lib_dir) / "npm"
245-
elif self.install_root is not None:
246-
npm_install_root = self.install_root / "npm"
247-
else:
248-
npm_install_root = None
249265
cli_provider = NpmProvider(
250266
install_root=npm_install_root,
251267
postinstall_scripts=effective_postinstall,
@@ -258,7 +274,6 @@ def setup(
258274
postinstall_scripts=effective_postinstall,
259275
min_release_age=effective_min_release_age,
260276
).install(no_cache=no_cache)
261-
self._INSTALLER_BINARY = cli # bootstrap: seed cache after npm install
262277
path_entries: list[Path] = []
263278
if self.bin_dir is not None:
264279
path_entries.append(self.bin_dir)
@@ -270,6 +285,10 @@ def setup(
270285
PATH="",
271286
prepend=True,
272287
)
288+
loaded_cli = self.load(self.INSTALLER_BIN, quiet=True, no_cache=True)
289+
self._INSTALLER_BINARY = (
290+
loaded_cli if loaded_cli is not None else cli
291+
) # bootstrap: seed cache after npm install
273292

274293
def _playwright_browser_path(
275294
self,
@@ -430,7 +449,7 @@ def default_install_handler(
430449
return f"DRY_RUN would run: playwright install {' '.join(merged_args)}"
431450

432451
effective_timeout = timeout if timeout is not None else self.install_timeout
433-
installer_bin = self.INSTALLER_BINARY(no_cache=no_cache).loaded_abspath
452+
installer_bin = self.INSTALLER_BINARY().loaded_abspath
434453
assert installer_bin
435454
install_cmd = ["install", *merged_args]
436455
# Retry on dpkg lock contention (apt-get may be held by a

0 commit comments

Comments
 (0)