Compare commits

...

2 Commits

Author SHA1 Message Date
2f237ac265 chore(release): v0.7.33 — coverage gate fix for PR-C local watcher wrapper
All checks were successful
boundary-lint / PR boundary-claim (Lint (push) Has been skipped
ci / test-health gate (push) Successful in 17s
boundary-lint / ban-list lint (Lint (push) Successful in 19s
ci / mutation test (broker) (push) Has been skipped
boundary-lint / duplication-deadline (Layer 1/2) (push) Successful in 19s
Release Publish (Gitea session_helper) / verify-release-tag (push) Successful in 17s
ci / rust debug (push) Successful in 2m13s
ci / rust release (push) Successful in 2m19s
ci / python (push) Successful in 1m52s
Release Publish (Gitea session_helper) / publish-linux-x86_64 (push) Successful in 4m14s
v0.7.32's PR-C added ``_rust_ffi/_local_watcher.py`` (49 stmts) without
Python-only wrapper tests; coverage dropped to 79.60% and the
``test-health gate`` CI step rejected the release. v0.7.33 bundles the
13-test follow-up that covers ``start`` / ``drain`` / ``stop``
contracts (handle pass-through, missing-symbol → SessionsNativeLibrary
Error, drain buffer-too-small retry, unit-separator decode).

No behaviour changes vs v0.7.32 — Rust binary, Python plugin code, and
filesystem watcher logic are byte-identical to the v0.7.32 release;
this re-publishes signed artifacts after the coverage gate accepts the
build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:53:49 +09:00
3a8e86ca6b test(rust_local_watcher): cover ctypes wrapper contracts (PR-C follow-up)
CI's coverage gate dropped to 79.60% (under the 80% threshold) because
the brand-new ``_rust_ffi/_local_watcher.py`` wrapper landed without
Python-only tests — Rust ABI smoke covers the live ``notify`` event
loop but the ctypes layer was uncovered.

Add ``test_rust_local_watcher.py`` (13 tests) following the same
pattern as ``test_rust_file_policy.py``: fake ``_native_lib()`` cdylib
with stub functions, exercise every wrapper path:

* ``start`` returns the Rust handle / 0 / raises on missing symbol
* ``drain`` decodes ``\\x1F``-joined payload, retries on
  buffer-too-small sentinel, returns ``()`` on negative rc / unknown
  handle / zero handle, raises on missing symbol
* ``stop`` returns bool from rc==1, short-circuits on zero handle,
  raises on missing symbol

Restores coverage above the gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:52:03 +09:00
5 changed files with 160 additions and 9 deletions

View File

@@ -1,6 +1,6 @@
[project]
name = "sessions-sublime"
version = "0.7.32"
version = "0.7.33"
description = "Sublime-facing Python code for Sessions."
requires-python = ">=3.8"
license = {text = "MIT"}

12
rust/Cargo.lock generated
View File

@@ -221,7 +221,7 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "local_bridge"
version = "0.7.32"
version = "0.7.33"
dependencies = [
"base64",
"glob",
@@ -432,7 +432,7 @@ dependencies = [
[[package]]
name = "session_helper"
version = "0.7.32"
version = "0.7.33"
dependencies = [
"base64",
"notify",
@@ -443,7 +443,7 @@ dependencies = [
[[package]]
name = "session_protocol"
version = "0.7.32"
version = "0.7.33"
dependencies = [
"base64",
"serde",
@@ -452,14 +452,14 @@ dependencies = [
[[package]]
name = "sessions_askpass"
version = "0.7.32"
version = "0.7.33"
dependencies = [
"tempfile",
]
[[package]]
name = "sessions_native"
version = "0.7.32"
version = "0.7.33"
dependencies = [
"base64",
"notify",
@@ -773,7 +773,7 @@ dependencies = [
[[package]]
name = "workspace_identity"
version = "0.7.32"
version = "0.7.33"
[[package]]
name = "zmij"

View File

@@ -12,7 +12,7 @@ resolver = "2"
[workspace.package]
edition = "2024"
license = "MIT"
version = "0.7.32"
version = "0.7.33"
authors = ["Myeongseon Choi <key262yek@gmail.com>"]
repository = "https://git.teahaven.kr/sublime-rs/sessions"
homepage = "https://git.teahaven.kr/sublime-rs/sessions"

View File

@@ -0,0 +1,151 @@
"""Tests for ``_rust_ffi.local_watcher`` wrapper contracts.
The Rust side of the watcher is exercised by
``sessions_native::local_watcher::tests`` (6 tests covering the live
``notify`` event loop, filtering, and stop idempotency). These
Python-only tests pin the ctypes-wrapper layer contract:
* ``start`` returns the integer the Rust ABI returned (handle on
success, 0 on failure).
* ``drain`` decodes the ``\\x1F``-joined payload, retries on the
buffer-too-small sentinel, returns ``()`` on negative rc or
zero/negative handle.
* ``stop`` returns ``True`` only when the Rust ABI returns ``1``.
* All three raise ``SessionsNativeLibraryError`` when the symbol is
missing from the cdylib.
"""
import ctypes
import pytest
from sessions import _rust_ffi
from sessions._rust_ffi import SessionsNativeLibraryError
def _install(monkeypatch, **symbols) -> None:
class _Lib:
pass
lib = _Lib()
for name, func in symbols.items():
setattr(lib, name, func)
monkeypatch.setattr(_rust_ffi._loader, "_native_lib", lambda: lib)
class _FakeIntFunc:
def __init__(self, rc: int) -> None:
self._rc = rc
self.argtypes = None
self.restype = None
def __call__(self, *args: object) -> int:
return self._rc
class _FakeDrainFunc:
"""Mimics ``sessions_local_watcher_drain``.
Returns ``rc`` and, when ``rc == 0``, writes ``payload`` (UTF-8 +
NUL terminator) into the caller's ``out_buf``. When ``rc > out_cap``
we expect the wrapper to retry with a bigger buffer.
"""
def __init__(self, *, rc: int = 0, payload: str = "") -> None:
self._rc = rc
self._payload = payload
self.argtypes = None
self.restype = None
self.calls: list[int] = []
def __call__(self, _handle: object, out_buf: object, out_cap: int) -> int:
self.calls.append(out_cap)
if self._rc != 0:
return self._rc
encoded = self._payload.encode("utf-8") + b"\x00"
if out_cap < len(encoded):
return len(encoded)
ctypes.memmove(out_buf, encoded, len(encoded))
return 0
def test_start_returns_handle_from_rust(monkeypatch, tmp_path) -> None:
_install(monkeypatch, sessions_local_watcher_start=_FakeIntFunc(rc=42))
assert _rust_ffi.local_watcher.start(str(tmp_path)) == 42
def test_start_returns_zero_on_failure(monkeypatch, tmp_path) -> None:
_install(monkeypatch, sessions_local_watcher_start=_FakeIntFunc(rc=0))
assert _rust_ffi.local_watcher.start(str(tmp_path)) == 0
def test_start_raises_when_symbol_missing(monkeypatch, tmp_path) -> None:
_install(monkeypatch) # no symbol bound
with pytest.raises(SessionsNativeLibraryError):
_rust_ffi.local_watcher.start(str(tmp_path))
def test_drain_with_zero_handle_short_circuits(monkeypatch) -> None:
# Should not even reach the Rust ABI; install a func that would
# explode if called.
_install(monkeypatch, sessions_local_watcher_drain=_FakeIntFunc(rc=-1))
assert _rust_ffi.local_watcher.drain(0) == ()
assert _rust_ffi.local_watcher.drain(-5) == ()
def test_drain_returns_empty_tuple_on_empty_payload(monkeypatch) -> None:
func = _FakeDrainFunc(rc=0, payload="")
_install(monkeypatch, sessions_local_watcher_drain=func)
assert _rust_ffi.local_watcher.drain(7) == ()
def test_drain_splits_unit_separator(monkeypatch) -> None:
func = _FakeDrainFunc(rc=0, payload="/a/b\x1f/c/d\x1f/e")
_install(monkeypatch, sessions_local_watcher_drain=func)
assert _rust_ffi.local_watcher.drain(1) == ("/a/b", "/c/d", "/e")
def test_drain_returns_empty_on_unknown_handle(monkeypatch) -> None:
# Rust returns -1 when ``handle`` is unknown ("watcher gone").
_install(monkeypatch, sessions_local_watcher_drain=_FakeIntFunc(rc=-1))
assert _rust_ffi.local_watcher.drain(99) == ()
def test_drain_grows_buffer_on_buffer_too_small(monkeypatch) -> None:
# First call returns the required size; second succeeds.
payload = "/long/path/" + "x" * 16_000
func = _FakeDrainFunc(rc=0, payload=payload)
_install(monkeypatch, sessions_local_watcher_drain=func)
out = _rust_ffi.local_watcher.drain(1)
assert out == (payload,)
# Two attempts: 8192 (initial), then >= encoded length.
assert len(func.calls) >= 2
assert func.calls[0] == 8192
assert func.calls[-1] >= len(payload.encode("utf-8")) + 1
def test_drain_raises_when_symbol_missing(monkeypatch) -> None:
_install(monkeypatch)
with pytest.raises(SessionsNativeLibraryError):
_rust_ffi.local_watcher.drain(1)
def test_stop_returns_true_when_rust_returned_one(monkeypatch) -> None:
_install(monkeypatch, sessions_local_watcher_stop=_FakeIntFunc(rc=1))
assert _rust_ffi.local_watcher.stop(1) is True
def test_stop_returns_false_when_rust_returned_zero(monkeypatch) -> None:
_install(monkeypatch, sessions_local_watcher_stop=_FakeIntFunc(rc=0))
assert _rust_ffi.local_watcher.stop(1) is False
def test_stop_with_zero_handle_short_circuits(monkeypatch) -> None:
_install(monkeypatch, sessions_local_watcher_stop=_FakeIntFunc(rc=99))
assert _rust_ffi.local_watcher.stop(0) is False
assert _rust_ffi.local_watcher.stop(-3) is False
def test_stop_raises_when_symbol_missing(monkeypatch) -> None:
_install(monkeypatch)
with pytest.raises(SessionsNativeLibraryError):
_rust_ffi.local_watcher.stop(7)

2
uv.lock generated
View File

@@ -854,7 +854,7 @@ wheels = [
[[package]]
name = "sessions-sublime"
version = "0.7.31"
version = "0.7.32"
source = { virtual = "." }
[package.dev-dependencies]